Miniprog
Hello, MiniProg 1 |
Раздел Подземелье Магов |
Я не знаю, к какой области "Королевства" отнести эту статью. В принципе, данная публикация подготавливалась для раздела "Hello, World".
Однако оказалось, что подавляющее количество достаточно опытных программистов, имеют лишь приблизительное понятие об изложенном материале. Почти полное отсутствие в интернете и литературе информации, по данной тематике и использованным в статье методам, оставляют надежду на то, что кому-то, возможно, будет интересно, а может быть и познавательно то, о чем здесь написано.
Первоначальная идея была проста, написать несложную программу, исходный текст которой можно было бы использовать как некий шаблон, с реализованной функциональностью, отвечающей наиболее часто выдвигаемым требованиям. Следует помнить о данной публикации то, что она навеяна темой форума "Delphi Kingdom VCL :)".
Желающие могут присоединиться: покритиковать, дополнить, исправить; и если изменения или найденные ошибки будут существенны, то будет написана новая статья.
Начнем, с требований, которым должна соответствовать программа:
1. Избегать использования компонентов сторонних производителей, стараться написать программу с помощью стандартных, для текущей версии Delphi, функций и процедур.
2. Исходный текст программы необходимо снабдить системой автоматической проверки корректности программного кода.
3. Интерфейс - SDI (MDI хорош для приложений вроде Word или Exceel).
4. Желательно предусмотреть возможность масштабирования размеров окон, а также размеров и положения всех визуальных элементов, расположенных на ней, после изменения размеров экранного шрифта. Размеры окон и визуальных компонентов не должны меняться при изменении разрешения экрана.
5. Программа должна следить за тем, что бы она была запущена в единственном числе, на данном компьютере, при этом при запуске второй копии должна активизироваться первая копия, даже если она находится в свернутом состоянии. Данная функция призвана защитить лишь от случайных или ошибочных действий пользователя, и ни как не претендует на роль "серьезной" защиты.
6. Программа должна иметь возможность запуска с командной строки, с использованием управляющих ключей.
7. Программа должна иметь возможность запуска, как в режиме консоли, так и в режиме с графическим интерфейсом.
8. Программа должна уметь показывать при запуске заставку, и иметь возможность, как изменения времени показа, так и полного отключения заставки. Данные режимы должны быть управляемы как с помощью командной строки, так и в режиме графического интерфейса.
9. Управляющие ключи командной строки, должны поддерживать, как минимум ключ "?" и/или "help"- вывод краткого пояснения о программе, и подсказки о доступных ключах, в режиме консоли. Ключ "concole" - запуск в режиме консоли. Ключ "nologo" - отключение показа заставки. Ключ "logo " c параметром, определяющим время показа заставки.
10. Необходимо предусмотреть возможность взаимодействия программы с конфигурационным файлом, для хранения и восстановления определенных параметров. Нужно уметь хранить в конфигурационном файле время показа заставки, а так же состояние и позицию окон.
11. Необходимо иметь возможность, в режиме с графическим интерфейсом, подключения к программе языковых настроек в виде перевода на различные языки надписей и сообщений. Основной язык программы английский.
Пункт 7 и все, что с ним связано, можно считать моим личным капризом, но мне приходится писать именно такие программы - исполняющиеся в обоих режимах. Для начала, реализации таких требований, должно хватить при создании приложений.
Ну что же, первый шаг, создание директории проекта, назовем его MiniProg, в которой расположим поддиректории:
DCU - откомпилированные dcu (такова моя привычка :), DOC - поместим текст данной статьи, DUNIT - система автоматического тестирования от SourceForge, IMAGE - для картинок и иконок, SOURCE - исходные тексты самой программы, TEST - исходные тексты тестирующих файлов. |
Будем считать, что основная директория, MiniProg, предназначена для размещения в ней откомпилированной программы, файлов конфигурации и языковых настроек, а так же, для откомпилированной тестовой программы.
Создаем проект, главную форму называем просто и незатейливо - FMain. Cохраняем как файл Main.pas в поддиректории SOURCE. Проект сохраняем как MiniProg.dpr, там же :). Открываем меню Project | Options, переходим на страницу Directories/Conditionals, заносим в Output directory и в Unit output directory соответствующие пути. В нашем случай это будут "..\..\MiniProg" и "..\..\MiniProg\DCU". Можно и короче записать, но так нагляднее. Если есть иконка для программы, то устанавливаем её на странице Application, через Load Icon. Создадим новый unit, сохраним под именем Appl.pas. Зачем? Как задел на будущее, будем размещать в нем функции и процедуры, реализующие наши требования.
Теперь начнем выполнять пункт 2 наших требований, т.е. создавать тестирующую программу. В подкаталоге DUNIT расположены некоторые необходимые нам файлы, взятые из оригинального DUNIT , версии от 2002/01/17. И так, создаем новый проект, закрываем Unit1.pas, отказываемся от сохранения, проект назовем, без особой фантазии, testMiniProg.dpr и сохраняем в TEST. Удаляем всё из этого файла и помещаем в него такой код:
program testMiniProg; uses Forms, TestFrameWork, GUITestRunner; {$R *.res} begin Application.Initialize; GUITestRunner.RunRegisteredTests; end. |
В настройках проекта, на странице Directories/Conditionals, заполняем поля Output directory и Unit output directory, так же, как и у проекта MiniProg. Дополнительно пропишем в Search path поддиректорий SOURCE и DUNIT. Вот теперь, можно создать новый unit с названием (как бы вы думали?) testAppl.pas и следующим содержанием:
unit testAppl; interface uses TestFramework, SysUtils, Controls, Forms, Appl; type TTestUnitAppl = class(TTestCase) published end; implementation initialization TestFramework.RegisterTest(TTestUnitAppl.Suite); end. |
Можно откомпилировать testMiniProg и посмотреть на внешний вид нашей тестирующей программы. В дереве просмотра, с именем Test Hierarchy, будут заноситься наши тесты, серые квадратики, при успешном прохождении теста, будут окрашиваться зеленым цветов, иначе - красным или розовым (цвет может быть и синим). Тесты можно отключать галочками. В окнах, расположенных ниже, можно будет наблюдать сообщение о всякой всячине, в том числе и некоторое пояснение о крахе теста. Да, кстати, тесты запускаются кнопочкой, с изображением зеленого треугольника, но пока он окрашен в серый цвет, так как ни одного реального теста у нас нет. Вот, вкратце и всё, что пока нужно знать о DUNIT. Товарищи, желающие узнать о DUNIT больше, а так же патологические "ХочуВсёЗнайки", могут самостоятельно поискать дополнительную информацию. Хочу только заметить, что данная система проверки является портом с JUNIT, и создавалась для применения в проектах с использованием Xtreem Programming (сокращенно XP). Одной из отличительных особенностей данной методологии является глубокая неприязнь к ведению документации :). Подробнее и по-русски можно посмотреть здесь , там же приведены ссылки по этой тематике. Конечно же, возможности DUNIT гораздо шире, чем это будет показано в данном материале (перед самым окончанием статьи была найдена интересная ссылка - по ней можно ознакомиться с более изощренным применением DUNIT).
Попытаемся разобраться с проблемой масштабирования форм. Проведя то, что обычно называется предварительным расследованием; покопавшись в интернет, заглянув в хелп, почитав книги, спросив товарищей (нужное подчеркнуть); выяснилось что, можно принудить форму автоматически масштабировать собственные размеры, а так же размеры и положение размещенных на ней визуальных компонентов, при изменении размера экранного шрифта. Для этого необходимо проверить и если нужно установить свойства формы, в нашем случае FMain, ParentFont = False, Scaled = True, AutoScroll = False и PixelsPerInch равный PixelsPerInch текущего экрана. Данное утверждение верно для форм созданных с помощью Delphi 6.2, для более ранних версий не проверялось. Но, судя по количеству воплей на различных форумах - у некоторых такая проблема была. Впрочем, помнится, еще у М. Канту в "Delphi 2 for Windows95/NT" существовала небольшая глава, освещающая именно такой подход. После рассмотрения исходных кодов VCL Delphi выяснилось, что существует другая проблема, связанная с масштабированием. Дело в том, что свойства Constraints компонентов, к большому сожалению, не масштабируются. Придется заняться этим отдельно, иначе может нарушиться внешний вид формы.
Что делает программа, когда создает форму? Если у формы установлены свойства как было указано выше, то в зависимости от того, отличается PixelsPerInch (сокращенно PPI) формы от PPI экрана или нет, происходит умножение значений местоположения компонентов на "новый" PPI и деление на "старый" PPI (в действительности, конечно, всё сложнее, но на первых порах и такого понимания достаточно). Будем называть эту функцию ScaleValue.
Откроем проект testMiniProg, и откроем в нем файлы testAppl.pas и Appl.pas из поддиректории SOURCE. Теперь самое странное: в testAppl.pas создаем процедуру проверки TestScaleValue, и объявляем её в published свойствах TTestUnitAppl:
unit testAppl; interface uses TestFramework, SysUtils, Controls, Forms, Appl; type TTestUnitAppl = class(TTestCase) published procedure TestScaleValue; end; implementation procedure TTestUnitAppl.TestScaleValue; var Test: integer; begin Test := ScaleValue(120,96,120); Check( Test = 96, Format('return wrong %d',[Test])); end; initialization TestFramework.RegisterTest(TTestUnitAppl.Suite); end. |
Главное действие в этом unit, происходит в теле процедуры TestScaleValue, по вызову функции Check, в которой проходит проверки первого параметра, и если он False, то тест считается неудачным. Второй параметр функции Check - сообщение, в котором можно написать, в краткой форме, всё, что вы думаете об отрицательном результате тестирования :). Почему, при заданных значениях входных параметров, в результате должно получиться именно 96? - можно понять в результате несложных математических преобразований исходной формулы. Менее успешные математики могут проверить на калькуляторе :). Что же, мы создали тестирующую процедуру, которая проверит корректность работы нашей функции, при чем сделает это автоматически, стоит лишь запустить тесты. Следует сказать, что проверяться функция будет при каждом запуске тестовой программы, т.е. если вы впоследствии поменяете текст функции, и сделаете это некорректно, то программа тут же сообщит вам об этом. Еще одним положительным свойством такого тестирования, является то, что в саму программу не вносится ни каких посторонних тестирующих и проверяющих функций. Далее, в файле Appl.pas, создаем саму функцию:
function ScaleValue(Value, NewPPI, OldPPI: integer): integer; begin Result := MulDiv(Value, NewPPI, OldPPI); end; |
Компилируем, запускаем программу, нажимаем на зеленый треугольник - всё зеленое! Замечательно, первый и пока единственный тест пройден. Если кто-то не заметил, то поясню, что сначала была создана тестирующая процедура, проверяющая результат функции, и только потом создавалась сама функция. Несколько необычно, но именно такой порядок рекомендует методология XP. Вообще, если призадуматься, то в этом можно узреть глубокий смысл, который заключен в том, что до создания функции мы ДОЛЖНЫ хорошо себе представлять результат :). Вроде бы тривиальная мысль, но многих ошибок в программах не было бы, если бы кодеры всегда следовали этому правилу. Подход, продемонстрированный выше, просто вынуждает поступать именно так. Другим положительным моментом предварительного создание тестовых функций является то что, в конечном счете, изначально "большие" функции будут разбиты на более мелкие и легко тестируемые, что то же неплохо. Кстати, XP настоятельно рекомендует заниматься рефакторингом, по-простому - переписыванием исходного текста, с целью его улучшения. Правда, в отличие от банального исправления ошибок и внесения уточнений, рефакторить рекомендуется только тогда, когда в этом действительно возникла необходимость. Но вообще, код пишется исключительно в требованиях текущего момента, т.е. даже если вы знаете, что какая то дополнительная функциональность вам обязательно понадобиться в дальнейшем - не прилагайте ни малейшего усилия, для её реализации. На этапе рефакторинга всегда можно вернуться к этому, если конечно понадобиться :).
Очевидно, что нам нужны функции, которые бы возвращали значения PPI как времени создания, так и времени исполнения программы, назовем их RtmPPI и DsgnPPI. Напишем тест. Подумав, решаем, что RtmPPI и DsgnPPI должны быть равны по значениям, если разработка программы и тестирование происходит при одних и тех же режима экрана:
procedure TTestUnitAppl.TestDsgnVsRtmPPI; begin Check( DsgnPPI = RtmPPI, Format('Design time PixelsPerInch not %d DPI', [RtmPPI])); end; |
По крайней мере, такой тест напомнит вам, что при тестировании значение DsgnPPI должно быть равно PPI вашего экрана. Один совет, связанный с масштабированием форм - старайтесь создавать все свои формы при одном и том же PPI, это убережет вас от неприятных эффектов в дальнейшем, либо вам придется написать специальный тест, который будет проверять значения PPI всех форм, а это часто очень утомительно :). Кстати, этот тест наводит на мысль о том, что не плохо было бы завести функцию, которая бы сообщала, изменилось PPI или нет, и она нам нужна именно сейчас, что бы включить в тест. Сам текст функций выглядит следующим образом:
function RtmPPI: integer; begin Result := Screen.PixelsPerInch; end; function DsgnPPI: integer; begin Result := 120; end; function IsChangePPI: boolean; begin Result := DsgnPPI <> RtmPPI; end; |
К сожалению, функция DsgnPPI возвращает результат, просто используя константу, которая выставляется в зависимости от конкретного PPI, используемого при дизайне (у меня это 120, у вас может быть и другое значение). Несмотря на то, что в хелп указано TForm.PixelsPerInch как свойство, хранящее значение времени создания, проверка показала, что это не так. Рассмотрение исходных текстов подтвердило факт изменения значения TForm.PixelsPerInch при масштабирование формы, во время исполнения. Так как простого и надежного решения данной проблемы у меня ПОКА нет, то поступим в соответствии с принципами Экстремального Программирования - "Если есть что-то что можно отложить на завтра - отложите это". Прошу прощение, у адептов XP, за столь вольную трактовку принципа.
Пришло время заняться процедурой, которая будет масштабировать Constraints компонентов. Собственно говоря, это свойство наследуется от TControl, по этому, будем обращаться именно к нему. Подумаем, как тестировать изменение Constraints. Первое, что приходит в голову, это создать специальную тестовую форму. Конечно, такой путь несколько сложноват, однако эта форма, скорее всего, пригодиться и в дальнейшем. Выбираем меню File | New | Form, даем название testForm и сохраняем как testUnit в поддиректории TEST, если Delphi предложит сохранить еще и проект, смело откажитесь. Не забудьте установить свойства формы так, как было описано ранее. Добавьте, в uses Appl. Проверьте, в меню Project | Options, новая форма должна располагаться в Available Forms, то есть не должна создаваться автоматически, при запуске приложения. Создайте в Events формы событие OnClose:
procedure TtestForm.FormClose(Sender: TObject; var Action: TCloseAction); begin Action := caFree; end; |
Это заставит удалиться форму из памяти самостоятельно, после закрытия. Не забудьте, выполнить, для testAppl.pas, дополнение через File | Use Unit: Вот, теперь создадим TestChangeConstraints. Что бы легче было тестировать, и избежать неоднозначности, воспользуемся опытом тестирования ScaleValue и зададим размеры формы кратные 120, например 480, после масштабирования должно получиться 384. Так как, отдельные числа используются в unit более чем один раз, то вынесем их в константы.
const testOldPPI = 120; testNewPPI = 96; ... procedure TTestUnitAppl.TestChangeConstraints; var OK1, OK2: boolean; Size1, Size2: integer; begin OK1 := False; OK2 := False; Size1 := testOldPPI * 4; Size2 := ScaleValue(Size1, testNewPPI, testOldPPI); testForm := TtestForm.Create(Application); try testForm.Constraints.MaxHeight := Size1; testForm.Constraints.MinHeight := Size1; testForm.Constraints.MaxWidth := 0; testForm.Constraints.MinWidth := Size1; ChangeConstraints(testForm as TControl, testNewPPI, testOldPPI); OK1 := (testForm.Constraints.MaxHeight = Size2) and (testForm.Constraints.MinHeight = Size2) and (testForm.Constraints.MaxWidth = 0) and (testForm.Constraints.MinWidth = Size2); ChangeConstraints(testForm as TControl, testOldPPI, testNewPPI); OK2 := (testForm.Constraints.MaxHeight = Size1) and (testForm.Constraints.MinHeight = Size1) and (testForm.Constraints.MaxWidth = 0) and (testForm.Constraints.MinWidth = Size1); finally testForm.Close; Check(OK1 and OK2, 'failed test'); end; end; |
Как видите, тест весьма незатейливый, проверяет корректность масштабирования, как при уменьшающем, так и при увеличивающем масштабе. А еще этот тест использует уже протестированную функцию, что в конечном счете добавляет уверенности в результаты теста :). Сама функция ChangeConstraints выглядит так:
procedure ChangeConstraints(Control: TControl; NewPPI, OldPPI: integer); begin with Control.Constraints do begin if MaxHeight > 0 then MaxHeight := ScaleValue(MaxHeight, NewPPI, OldPPI); if MinHeight > 0 then MinHeight := ScaleValue(MinHeight, NewPPI, OldPPI); if MaxWidth > 0 then MaxWidth := ScaleValue(MaxWidth, NewPPI, OldPPI); if MinWidth > 0 then MinWidth := ScaleValue(MinWidth, NewPPI, OldPPI); end; end; |
Запускаем тест - "Шеф!!! Всё пропало!!!" - в чем же дело? А дело в том, что Constraints для минимальных и максимальных значений взаимозависимы. Максимальное значение не может быть меньше минимального и наоборот, и если происходит присвоение некорректного, с этой точки зрения значения, то оно изменяется в нужную сторону. Такое поведение весьма логично, но нас оно не всегда устраивает, по тому что, нам бы хотелось, что бы такое выравнивание сработало после наших изменений. Кстати, вот вам и первый пойманный баг, и довольно хитрый :). Поспешный поиск дихлофоса от Borland. , среди методов TControl, напоминавших по духу, что-то вроде DisabledAlign ничего не дал. Пришлось воспользоваться простым дедовским антитараканным средством - типа "тапочек":
procedure ChangeConstraints(Control: TControl; NewPPI, OldPPI: integer); begin with Control.Constraints do begin if NewPPI > OldPPI then begin if MaxHeight > 0 then MaxHeight := ScaleValue(MaxHeight, NewPPI, OldPPI); if MinHeight > 0 then MinHeight := ScaleValue(MinHeight, NewPPI, OldPPI); if MaxWidth > 0 then MaxWidth := ScaleValue(MaxWidth, NewPPI, OldPPI); if MinWidth > 0 then MinWidth := ScaleValue(MinWidth, NewPPI, OldPPI); end else begin if MinHeight > 0 then MinHeight := ScaleValue(MinHeight, NewPPI, OldPPI); if MaxHeight > 0 then MaxHeight := ScaleValue(MaxHeight, NewPPI, OldPPI); if MinWidth > 0 then MinWidth := ScaleValue(MinWidth, NewPPI, OldPPI); if MaxWidth > 0 then MaxWidth := ScaleValue(MaxWidth, NewPPI, OldPPI); end; end; end; |
Тест, зеленый цвет, "едем" дальше... Дальше? А дальше, расположим на testForm какие-нибудь визуальные компоненты, ...даааа побольше :). В принципе, TestChangeConstraints показал, что процедура работает успешно, с наследником TForm, но не мешало бы, проверить её и с другими компонентами, хотя бы некоторую их часть (нет у нас такого требования - тестировать VCL). Так как предполагаемый процесс тестирования вполне однообразен, то создадим функцию, которой будем передавать компонент, из числа тех, которые расположены на форме, а возвращать она будет - "да" или "нет".
function TTestUnitAppl.TestScaleControl(Control: TControl): boolean; var OK1, OK2: boolean; Size1, Size2: integer; begin OK1 := False; OK2 := False; Size1 := testOldPPI; Size2 := ScaleValue(Size1, testNewPPI, testOldPPI); testForm := TtestForm.Create(Application); try Control.Constraints.MaxHeight := Size1; Control.Constraints.MinHeight := 0; Control.Constraints.MaxWidth := Size1; Control.Constraints.MinWidth := Size1; ChangeConstraints(Control, testNewPPI, testOldPPI); OK1 := (Control.Constraints.MaxHeight = Size2) and (Control.Constraints.MinHeight = 0) and (Control.Constraints.MaxWidth = Size2) and (Control.Constraints.MinWidth = Size2); ChangeConstraints(Control, testOldPPI, testNewPPI); OK2 := (Control.Constraints.MaxHeight = Size1) and (Control.Constraints.MinHeight = 0) and (Control.Constraints.MaxWidth = Size1) and (Control.Constraints.MinWidth = Size1); finally testForm.Close; Result := OK1 and OK2; end; end; |
Тестовая функция, например, для Label1, будет выглядеть так:
procedure TTestUnitAppl.TestScaleLabel; begin Check(TestScaleControl(testForm.Label1 as TControl), 'failed test '); end; |
Если все тесты проходят успешно, то с определенной долей вероятности можно утверждать, что мы теперь знаем, как настроить форму так, что бы она автоматически масштабировались, по крайней мере в пределах, которые обеспечивает Delphi. Так же сможем масштабировать Constraints отдельно взятого контрола окна, при необходимости. Думаю, сфера использования ChangeConstraints довольно ограниченна, но в большинстве случаев результаты, полученные с помощью таких простых средств - вполне удовлетворительные. Можно было бы разработать функцию, которая бы сама изменяла Constraints у всех элементов формы. Желающие могут попробовать свои силы самостоятельно, не забудьте только прислать пример с тестом, и он будет включен в проект. По моему скромному мнению, решить эту проблему кардинально и качественно, можно лишь на уровне изменения исходного кода VCL. Хотя, "неумение" Constraints корректировать свои значения во время масштабирование окна и не является "официальным" багом но, очень хочется надеяться, что а можно создать собственный вариант формы, в котором проблема будет решена, но для данного проекта это будет расцениваться как выход за рамки требований (см. пункт.1). Впрочем, повторюсь, если у кого-то есть возможность исправить - пишите.
И так, мы провели некоторые технологические тесты, и убедились в работоспособности функций и процедур основной программы. Пришло время заняться функциональными тестами, то есть тестами, в которых проводится общая проверка на соответствие наших решений требованиям. Наиболее наблюдательные читатели должны были заметить, что к самой программе мы еще и не прикасались, но уже имеем для неё несколько работоспособных функций :). Проводить функциональное тестирование можно по-разному, и в принципе, лучше всего на рабочем приложении. У нас, его пока нет, кроме того, оценить правильность масштабирования можно на любом примере, ведь от нас не требуется реакция (нажатие кнопки, движение мыши и т.д.). Нам нужно просто посмотреть. Так что, воспользуемся testForm и разместим на ней 3 компонента TLabel. В свойство Caption каждой занесем такой текст "0123456789". У Label2 установим Constraints равными Width, у Label3 минимальное и максимальное значения отличающееся не менее чем на 50%, у Label4 минимальное и максимальное значения отличающееся на 5%.
procedure TTestUnitAppl.TestFuncScale; begin if IsChangePPI then begin testForm := TtestForm.Create(Application); try testForm.ShowModal; finally testForm.Close; end; end; Check(True, 'very strange '); end; |
Тест очень прост, создается и визуализируется окно, рассматривается и закрывается. Процедура выполняется при запуске тестовой программы, если установлено иное значение PPI, чем использовалось при создании. И она всегда завершается успешно, что бы не портить общие "показатели" :). Можно откомпилировать тестовую программу, изменить размер шрифта экрана, перезагрузиться, запустить тест. Естественно, наш тест TestDsgnVsRtmPPI не должен пройти. Зато появиться окно testForm, где можно будет видеть результат масштабирования. Скажу прямо, LabeledEdit меня крайне разочаровал, впрочем, я его всегда подозревал и никогда им непользовался. Зато Label'ы вели себя так как им предписано. Закрываем окно, изменяем шрифт экрана, перезагружаемся, запускаем Delphi. Дальнейшие ухищрения в процессе тестирования, уважаемый читатель, может продолжить и самостоятельно.
Продолжение следует ...
Declaimer aka Отмазка. |
Я надеюсь, что люди, привыкшие читать академические труды, или слушать классические оперы, не станут осуждать простую и незатейливую песнь кочевника. Что делал - о том и пел.
Исходную партитуру и ноты можно взять .
Любые претензии и предложения принимаются в обсуждении. Предложения будут рассмотрены, претензии - проигнорированы.
С особым вниманием будут рассмотрены уточнения списка требований и новые тесты.
Все копирайты, если они известны, указаны. Иначе, автор не известен или копирайт утерян.
Brodia@a
Специально для
Проект создан в Delphi6 (44.3K)