Как сделать интерфейс в delphi
Создание и использование интерфейса
Тема интерфейсов достаточно обширна и интересна. В этой главе даются лишь самые общие сведения об интерфейсах. Сведение этой темы в одну главу с классами не случайно, т. к. интерфейс представляет собой пустой класс, т. е. класс, в котором провозглашены, но никак не расшифрованы свойства и методы.
Интерфейсы представляют собой частный случай описания типов. Они объявляются с помощью зарезервированного слова interface. Например:
Такое объявление эквивалентно описанию абстрактного класса в том смысле, что провозглашение интерфейса не требует расшифровки объявленных в нем свойств и методов.
В отличие от классов интерфейс не может содержать поля, и, следовательно, объявляемые в нем свойства в разделах read и write могут ссылаться только на методы. Все объявляемые в интерфейсе члены размещаются в единственной секции public. Методы не могут быть абстрактными (abstract), виртуальными (virtual), динамическими (dynamic) или перекрываемыми (override). Интерфейсы не могут иметь конструкторов или деструкторов, т. к. описываемые в них методы реализуются только в рамках поддерживающих их классов, которые называются интерфейсными.
Если какой-либо класс поддерживает интерфейс (т. е. является интерфейсным), имя этого интерфейса указывается при объявлении класса в списке его родителей:
В отличие от обычного класса интерфейсный класс может иметь более одного родительского интерфейса:
В любом случае в разделе реализации интерфейсного класса необходимо описать соответствующие интерфейсные методы. Если, например, объявлен интерфейс
и использующий его интерфейсный класс
то в разделе implementation следует указать реализацию методов:
Теперь можно объявить интерфейсный, объект класса TPainter, чтобы с его помощью нарисовать окружность и квадрат:
Несмотря на то что интерфейс всегда объявляется до объявления использующего его интерфейсного класса и, следовательно, известен компилятору, его методы обязательно должны быть перечислены в объявлении класса. В нашем случае простое указание
было бы ошибкой: компилятор потребовал бы вставить описание методов CirclePaint и RectPaint.
Подобно тому как все классы в Object Pascal порождены от единственного родителя TObject, все интерфейсные классы порождены от общего предка TInterfacedObject. Этот предок умеет распределять память для интерфейсных объектов и использует глобальный интерфейс lunknow:
Если бы в предыдущем примере класс TPainter был описан так:
Если интерфейс предполагается использовать в технологиях COM/DCOM или CORBA, его методы должны описывать с директивой stdcall или (для объектов Автоматизации) safecall
К интерфейсному объекту можно применить оператор приведения типов as, чтобы использовать нужный интерфейс:
Встретив такое присваивание, компилятор создаст код, с помощью которого вызывается метод Queryinterface интерфейса IUnknow с требованием вернуть ссылку на интерфейс IPaint. Если объект не поддерживает указанный интерфейс, возникает исключительная ситуация.
Интерфейсы, рассчитанные на использование в удаленных объектах, должны снабжаться глобально-уникальным идентификатором (guiD). Например:
Глобально-уникальные идентификаторы создаются по специальной технологии, гарантирующей ничтожно малую вероятность того, что два guid совпадут. Эта технология включена в Windows 32: чтобы получить guid для вновь созданного интерфейса в среде Delphi, достаточно нажать клавиши Ctrl+Shift+G. Для работы с guid в модуле System объявлены следующие типы:
Программист может объявлять типизированные константы типа tguid, например:
Константы guid могут использоваться вместо имен интерфейсов при вызове подпрограмм. Например, два следующих обращения идентичны:
С помощью зарезервированного слова implements программист может делегировать какому-либо свойству некоторого класса полномочия интерфейса. Это свойство должно иметь тип интерфейса или класса. Если свойство имеет тип интерфейса, имя этого интерфейса должно указываться в списке родителей класса, как если бы это был интерфейсный класс:
Обратите внимание: в этом примере класс TMyciass не является интерфейсным, т. е. классом, в котором исполняются методы p1 и P2. Однако если из него убрать определение уполномоченного свойства Mylnterface, он станет интерфейсным, и в нем должны быть описаны методы интерфейса IMylnterface.
Уполномоченное свойство обязательно должно иметь часть read. Если оно имеет тип класса, класс, в котором оно объявлено, не может иметь других уполномоченных свойств.
| Записи с максимаркетс отзывы’ вы можете найти на сайте | Метка: максимаркетс мошенники на сайте |
Трюки с интерфейсами в Delphi
Трюк 1. Умные Weak ссылки
Для тех кто не в курсе — Weak (слабые) ссылки — ссылки, не увеличивающие счетчик. Допустим у нас есть дерево:
Если бы внутри класса, реализующего интерфейс INode родитель и потомки хранились бы так:
то дерево бы никогда не уничтожилось. Родитель держит ссылки на детей (и тем самым увеличивает им счетчик), а дети на родителя. Это классическая проблема циклических ссылок, и в этом случае прибегают к weak ссылкам. В новых XE делфях можно написать так:
а в старых — хранят Pointer:
Это позволяет обойти автоинкремент счетчиков, и теперь если мы потеряем указатель на родителя — все дерево прибьется, что и требовалось получить.
У weak ссылок есть другая сторона. Если вдруг у вас уничтожился объект, а кто-то держит на него weak ссылку — вы не можете это отследить. По факту — у вас просто мусорный указатель, при обращении по которому будет ошибка. И это ужасно. Нужно лепить какую-то систему чистки этих самых ссылок.
Но есть очень элегантное решение. И вот как это работает. Мы пишем интерфейс weak ссылки и класс, реализующий его:
Тут обычный typecast до Pointer-а. Именно та weak ссылка, о которой я рассказывал выше. Но ключевой метод — IsAlive, который возвращает True — если объект на который ссылается weak ссылка — еще существует. Осталось только понять как красиво почистить FOwner.
Пишем интерфейс:
который возвращает weak ссылку и пишем класс, реализующий этот интерфейс:
Мы просто добавили метод, раздающий всем одну weak ссылку. А поскольку сам объект всегда знает о своей weak ссылке — он просто чистит её в своем деструкторе. Осталось теперь только наследоваться от TWeaklyInterfacedObject вместо TInterfacedObject, и все. Никаких больше unsafe приведений типов, выстрелов в ногу, и нецензурной брани.
Трюк 2. Механизм подписчиков
Если вы еще не велосипедили систему плагинов в делфи и не использовали MVC паттернов — то вы счастливчик. В делфи все события — это просто один или два указателя на функцию(и инстанс). Поэтому если вы создали класс, сделали ему OnBlaBla свойство — то только кто-то один может узнать, что этот самый BlaBla наконец то произошел. Посему все начинают пилить свой механизм подписок, и часто тонут в отладке этих самых подписок.
События основанные на интерфейсах обычно реализуют так. Делают отдельный евент интерфейс, к примеру:
и передают его, вместо классического procedure of object; например в пару Subscribe/Unsubscribe методов:
Когда код разрастается, а интерфейс IMouseEvents чуть-чуть меняется (например добавили метод) — начинает сильно напрягать рефакторинг. Например один и тот же IMouseEvents используется в IForm, IButton, IImage и прочей нечисти. Везде надо правильно поправить подписку, добавить обход по подписчикам и т.п.
Я использую следующий трюк. Пишем интерфейс:
Класс который будет реализовывать этот интерфейс (пусть это будет TBasePublisher) умеет только добавлять и удалять из списка какие-то интерфейсы. В дальнейшем мы пишем классы, которые я называю броадкастеры. Вот у нас есть евент интерфейс:
Мы наследуемся от TBasePublisher и реализуем вот такой броадкастер:
то есть сам броадкастер у нас реализует евент интерфейс, и в реализации просто рассылает всем подписчикам тот же евент. Преимущество — все реализовано в одном месте, оно не скомпилируется если вы хоть немного поменяете IGraphEvents. Теперь зоопарк IForm, IButton, IImage просто создают внутри себя TGraphEventsBroadcaster и вызывают его методы, как будто у IForm всего один подписчик.
Трюк 3. Умные Weak ссылки + механизм подписчиков
Но все что я описал выше про подписчиков — плохо. Дело в том, что тут сплошь и рядом будут циклические ссылки, вы замахаетесь разбираться с порядком финализации и отписыванием. Вы добавите слабые ссылки, но погрязнете в отладке мусорных ссылок. Вот тут то и пригодятся умные слабые ссылки, описанные в самом начале. Мы просто пишем вот такой интерфейс издателя (который принимает IWeakly из начала статьи):
Внутри себя издатель TBasePublisher хранит массив слабых ссылок TWeakRefArr = array of IWeakRef;
А броадкастер теперь только проверяет слабую ссылку на жизнеспособность, получает нормальную, и направляет евент в неё. Броадкастер поменялся вот так:
Теперь нас абсолютно не заботит порядок отписывания. Если мы забыли отписаться — ничего страшного. Все стало прозрачно, как в дотнете и должно было быть.
Трюк 4. Перегрузка в помощь
Последний штрих:
Я думаю он понятен без слов. Мы просто делаем MyForm.MyEvents + MySubscriber; — мы подписались. Вычли: MyForm.MyEvents — MySubscriber; — отписались.
Статья была бы не полной, если бы я не предоставил пример того как это работает. Поэтому вот пример. В кратце:
Программа создает 4 окна. На любом из окон можно рисовать мышкой. Нарисованные объекты добавляются в список, и через механизм подписок все окна уведомляются о изменении. Поэтому нарисованная фигура появляется на всех формах. На каждой форме можно выбрать собственную толщину линий с помощью трекбара.
Обобщенные интерфейсы в Delphi
Первод стаьи от Malcolm Groves, «Generic Interfaces in Delphi».
Большинство примеров использования дженериков в Delphi используют класс с дженерик-типом. Однако, работая над своим проектом, я решил, что мне нужен интерфейс с дженерик-типом.
В проекте используется встроенный механизм издатель-подписчик. Я захотел чтобы подписчик имел для каждого типа события отдельный метод Receive, а не отдельный метод с огромным case-выражением, выбирающим действие для каждого типа события. Также я не хотел определять интерфейс для каждого типа события. Мне был нужен дженерик интерфейс подписчика, который получает тип события, как параметр.
Однако, я понятия не имел, могу ли я определить дженерик интерфейс, не говоря уже о реализации. Даже если предположить, что я могу сделать это, сможет ли Delphi выбрать правильный метод Receive для вызова? Есть только один способ узнать…
Обратите внимание: в этом примере убраны некоторые части кода, оставлены лишь те части, которые необходимы для демонстрации дженерик интерфейсов. О других частях я расскажу в своих следующих сообщениях.
Сперва я описал несколько простых событий. Их содержание не так интересно:
TSomeEvent = class
// other stuff
end;
TSomeOtherEvent = class
// other stuff
end;
Затем, я определил дженерик интерфейс
ISubscriber = interface
procedure Receive(Event : T);
end;
Этот интерфейс должен быть реализован подписчиками для получения событий разного типа. Заметьте, тип событий записан как джененрик тип T.
Затем, подписчики должны реализовать интерфейс для каждого типа событий, которые они хотят принимать. Поскольку это дженерик интерфейс — это довольно просто:
Здесь нет описания интерфейсов ISomeEventSubscriber и ISomeOtherEventSubscriber, мы можем просто использовать ISubscriber и передать тип на месте. Для этого нужно реализовать обязательно перегруженный метод Receive.
Вышеприведенный код описывает основу задумки. Остальной код вы найдете в соответствующем тестовом проекте. Реализуем несколько интерфейсов, каждый из которых имеет строго типизированные Receive события, без определения каждого из этих интерфейсов фактически.
И это работает? С первой попытки — нет, не получилось. Независимо от того, какой тип события и через какой интерфейс я передавал, всегда выполнялся последний метод Receive.
dunit_generic_interfaces Жизненное правило №37: Если речь идет о выборе между запутывающимся Малькольм и разработчиками компилятора Delphi, вероятно, это ошибка Малькольма.
Удалил GUID и «as» — все заработало замечательно.
В будущих сообщениях покажу другие части: издателя и Брокера событий. Интересный побочный эффект определения событий, которыми интерисуется класс, в том, что брокер событий может просто проверить интерфейсы реализованные подписчиками, чтобы узнать на какие события они подписаны.
Создание графического интерфейса проекта в Delphi
Создадим проект, в котором на форму помещается изображение солнца.
Для этого с помощью графического редактора Paint нарисуем солнце и сохраним его в папке с нашим проектом, с именем Sun.bmp. Всем файлам, используемым в Delphi, лучше давать имена, записанные латинскими буквами.
Рис. 33 Сохранение рисунка командой [Правка – Копировать в файл].
Если имеется лишняя пустая область, то выделим нужное изображение солнца и выполним команду [Правка – Копировать в файл].
На форму поместим компонент TImage и загрузим в свойство Picture созданный нами рисунок Sun.bmp. Установим соответствующий фон формы и зададим свойство Transparent (прозрачность) у компонента Image1 равным true.
Рис. 34 Форма с компонентом TImage.
Наш рисунок не поместился в компонент, поэтому поэкспериментируем со свойствами Stretch, AutoSize или Proportional объекта TImage. При этом получится одно из следующих оформлений окна.
а) б)
в)
Рис. 35 Изменение размеров изображения различными способами
а) устанавливая свойству Stretch значение True.
б) устанавливая свойству AutoSize значение True.
в) устанавливая свойству Proportional значение True.
Задания
Рис. 36 Графический интерфейс окон
Рис. 37 Графический интерфейс окон
Реакция на события
Созданные нами окна никак не реагировали на какие-либо действия. Для создания «работоспособного» окна нужно вносить изменения в программный код.
Каждый компонент или элемент управления может реагировать на различные события. Однако есть события, например, Click (щелчок), на которые реагирует большинство элементов управления. Рассмотрим работу этого события.
Пример. Составить программу, которая завершает работу по щелчку на кнопке. |
Создадим графический интерфейс проекта, для этого поместим на форму компонент TButton (Кнопка). При щелчке именно по этой кнопке окно должно закрываться. С помощью Инспектора объектов изменим имя кнопки с Button1 на TClose, заголовок с Button1 на Выход. Можно также поменять параметры шрифта, чтобы заголовок был хорошо виден.
Рис. 38 Графический интерфейс окна
В Инспекторе объектов перейдем на закладку Events (События). За щелчок по любому компоненту, в том числе и по кнопке отвечает событие OnClick. Щёлкнем дважды по этому событию у кнопки Выход (можно также дважды щелкнуть по самой кнопке Выход). После этого Delphi создаст в редакторе кода процедуру – обработчик события OnClick.
procedure TForm1.TCloseClick(Sender: TObject); |
Begin |
end; |
По умолчанию имя процедуры начинается с имени класса TForm1, к которому относится данная процедура. Далее через точку записывается имя компонента TClose и имя события Click. В описании класса эта процедура объявляется автоматически (рис. 39).
Рис. 39 Объявление процедуры в описании класса TForm1
В окне Инспектора объектов напротив строки OnClick также должно появиться имя процедуры обработчика.
Рис. 40 Закладка Events
с именем обработчика события.
Созданная процедура имеет только один параметр Sender объектного типа. В начале выполнения процедуры в переменной Sender будет находиться указатель на объект, который вызвал этот обработчик. В данной программе в переменной Sender будет находиться указатель на кнопку TClose.
Напишем внутри процедуры (между begin и end) команду Close. Эта команда закрывает окно. Теперь процедура обработчика будет выглядеть так:
procedure TForm1.TCloseClick(Sender: TObject); |
Begin |
Close; |
end; |
После запуска программы окно может закрываться как по щелчку на кнопке Выход, так и по щелчку на крестике в системном меню.
В таблице №2 приведены основные события, на которые реагирует форма и большинство компонентов Delphi.
Таблица 2. Основные события Delphi
MVC-подход к реализации пользовательского интерфейса в Delphi. Часть 3. Объекты
В предыдущих частях статьи (1, 2) я показал, каким образом можно организовать работу с внутренними данными приложения и пользовательским интерфейсом через одну точку входа — модель. Изменения модели автоматически отражались в пользовательском интерфейсе. При этом для упрощения в качестве модели я использовал простые property класса формы, setter которых может привести GUI интерфейс к текущему состоянию модели. В данной части статья я покажу, как интерфейс может реагировать на изменения самих объектов внутри приложения.
Начать данную статью я бы хотел с рассмотрения ошибки, а точнее с неточности, допущенной в предыдущей части статьи. Приведенный там код добавления и удаления ролей у текущего выбранного пользователя корректно изменял внутреннее состояние объекта, но никак не обновлял при этом пользовательский интерфейс. Точнее интерфейс мог бы обновиться лишь после переключения с одного пользователя на другого и обратно. Как верно подметили в комментариях, для исправления данной недоработки достаточно было вставить в код процедур btAddRoleClick, btDelRoleClick вызов метода FillUserRoles. Работать будет, но это совсем не то, что нам нужно. Такой способ плох тем, что во всех местах, где роли сотрудника могут потенциально меняться, нужно каждый раз вставлять вызов по обновлению пользовательского интерфейса. А хочется раз и навсегда забыть о необходимости что-то делать с GUI в тех местах, где мы работаем с объектом. Я хочу, чтобы GUI реагировал на изменения объекта сам и сам перерисовывался, когда я изменяю поля объекта.
Для этого я расширю класс TUser следующим образом:
Я добавил в объект TUser простейшее нотифицирующее событие, которое будет уведомлять нас об изменении списка ролей сотрудника. При этом метод SetRoles класса TUser примет следующий вид:
Пока событие OnChangeRoles класса TUser не переопределено (по умолчанию FOnChangeRoles имеет значение nil), вызов DoChangeRoles просто ничего не делает. Для того, чтобы можно было как-то реагировать на данное событие, нужно присвоить объектам TUser соответствующий обработчик.
Этот обработчик логично завести у класса формы:
Теперь нужно навесить этот обработчик события на объекты класса TUser:
Вот вобщем-то и все :). Теперь при изменении ролей объекта будет срабатывать событие OnChangeRoles, назначенный обработчик которого будет вызывать FillUserRoles и обновлять GUI (перезаполнять список ролей). С этими правками код из предыдущей статьи будет работать корректно.
Можно ли было сделать лучше?
1) В контексте предыдущей статьи мне нужно было реагировать только на изменение списка ролей, поэтому я завел конкретное событие, реагирующее только на изменение поля Roles класса TUser. Зачастую реагировать нужно на изменение не одного, а нескольких (а может быть и всех) полей объекта. В этом случае лучше было завести событие не OnChangeRoles, а просто OnChange, правда и обработчик его в этом случае должен не только перестраивать список ролей, но и обновлять любую другую информацию о пользователе, которая могла в это время отображаться в окне. Соответственно и вызов DoChange находился бы не только в SetRoles, а также и в setter’ах остальных полей объекта TUser, изменения которых хотелось бы отслеживать. И здесь главная задача не забыть добавить этот вызов DoChange при добавлении нового поля к объекту, т.к. пропустить его довольно легко.
2) Исходя из принципов безопасного программирования, если мы регистрируем обработчик события (как еще говорят, «подписываемся» на событие), то мы должны потом эту подписку убрать («разрегистрировать» обработчик), т.е. вернуть OnChangeRoles в исходное состояние или на худой конец в nil. Нужно ли выполнять эту разрегистрацию, в каждом случае решается индивидуально. В первую очередь это зависит от соотношения времени жизни объектов TUser и объекта формы. Если форма живет дольше TUser’а, то в принципе разрегистрация не обязательна. Если же, наоборот, TUser может еще пожить и после уничтожения формы, то конечно в OnDestroy у формы нужно прописать что-то в духе
Если этого не сделать, то при попытке изменения объекта TUser после уничтожения формы TUser может попытаться вызвать обработчик события, ссылающийся на метод уже уничтоженного объекта (формы) и в лучшем случае мы получим Access Violation.
3) Когда мы работаем со списками объектов, присваивать обработчик каждому объекту не всегда удобно. Если элементы списка знают о самом списке (например, ссылаются на него через Owner’а), можно сделать, чтобы DoChange объектов TUser просто вызывал Owner.DoChange, а настраиваемое событие (property FOnChange) завести уже у самого списка (у TObjectList’а). Хотя это вобщем-то ничего по смыслу не меняет.
Рассмотренный способ можно считать подпиской на уведомления с одним подписчиком. Однако, это еще не полноценная подписка на уведомления. Уведомления хороши тем, что на них можно подписать сколько угодно подписчиков. Сейчас мы рассмотрим, как это делается. Давайте переключимся на другую задачу.
Уведомления с несколькими подписчиками
Данный шаблон очень часто применяется в качественно написанных MDI-приложениях (да и вообще в любых многооконных приложениях). Шаблон используется, когда в нескольких окнах системы могут отображаться одни и те же данные и при изменении этих данных через одно окно нужно чтобы они синхронно обновлялись во всех окнах. При этом данные окна не обязательно являются экземплярами окна одного класса и не обязательно имеют одинаковый пользовательский интерфейс. Напротив, окна могут быть совершенно разными. Они лишь отображают одну и ту же информацию. Например, в одном окне отображается список сотрудников, а в другом — карточка этого сотрудника, где можно изменить какие-то его характеристики. При этом требуется, чтобы по нажатию кнопки «Сохранить» в карточке сотрудника данные обновлялись бы как в карточке сотрудника, так и в общем списке сотрудников.
Шаблон множественной подписки на уведомления удобно применять при наличии долгоживущего объекта. Его время жизни должно быть заведомо больше времени жизни тех объектов, которые подписываются на уведомления от него. Допустим, у нас есть какой-то класс-менеджер, отвечающий за работу с сотрудниками (в частности за сохранение изменений объектов TUser в базу):
На вызовы SaveUser хотят реагировать все окна, в которых может отображаться какая-либо относящаяся к сотруднику информация. В этом случае классу TUserMngr придется хранить ссылки на все обработчики, которые могут подписаться на событие сохранения сотрудника:
Имея такой функционал, вы можете легко подписаться на изменения интересующих вас объектов из любого окна:
Теперь, когда мы поняли, как это будет использоваться, вернемся непосредственно к моменту нотификации, т.е. к моменту срабатывания события:
Теперь при сохранении объекта TUser все формы будут уведомляться об этом, если они не забыли подписаться на соответствующее событие.
Блокировка срабатывания обработчиков
Приведенный выше код является хорошим до тех пор, пока в системе не появляются операции сразу над большим количеством объектов. Возможно, не самый лучший пример: группа сотрудников прошла обучение и каждый из них получил какой-то одинаковый для всех сертификат. Мы выделяем 10 сотрудников в списке, жмем «Добавить сертификат». Далее поочередно происходит вызов UserMngr.Save для каждого из этих 10 сотрудников. При этом после сохранения каждого сотрудника срабатывает событие изменения DoUserChangeNotify, которое приводит к перестроению списка сотрудников во всех открытых окнах (а каждое перестроение будет еще приводить к перезапросу списка сотрудников из БД или с сервера приложений). В итоге сохранение изменений для 10 сотрудников будет происходить оооочень медленно и вдобавок мы получим массу миганий в открытых окнах приложения (списки будут перестраиваться по 10 раз). Сейчас я опишу простой способ, как этого избежать:
Метод нотификации при этом тоже изменится:
Через FLock отслеживается уровень блокировки (допускаются вложенные вызовы BeginUpdate..EndUpdate). FChanged — это флажок, позволяющий нам запомнить, происходило ли хотя бы один раз срабатывание события внутри сеанса блокировки. Если оно действительно происходило, то в момент выхода из сеанса блокировки (т.е. в момент вызова EndUpdate самого верхнего уровня), событие будет автоматически вызвано.
Таким образом, код изменения множества объектов можно легко защитить от излишних срабатываний событий:
Подобную блокировку удобно применять и в других случаях, например, когда нужно перевести объект из одного состояния в другое, изменив при этом не одно, а несколько его полей. При этом некоторые промежуточные состояния объекта (некоторые комбинации значений полей) могут считаться недопустимыми с точки зрения GUI. Соответственно нужно не допустить, чтобы GUI вообще узнал о том, что объект проходил через такие состояния. В таком случае изменение объекта также проводится внутри сеанса его обновления, когда срабатывание событий об изменении этого объекта заблокировано.
События — один из хороших приемов для связи объектов с GUI. Данный шаблон применяется не только при программировании GUI, но и во многих других случаях. В статье мы рассмотрели варианты реализации подписки на уведомления с одним и с множественными подписчиками. На этом цикл статей о программировании GUI в MVC-стиле скорее всего будет завершен. Если у кого-то остались вопросы именно по подходам к реализации GUI в Delphi, прошу оставлять их в комментариях и, возможно, данный цикл статей будет успешно продолжен. Также предлагаю в комментариях (а может и в отдельных статьях!) делиться своими приемами успешной реализации типовых задач на Delphi. И не надо закапывать никаких стюардесс, Delphi еще поживет 😉