
Предыдущая ��татья была посвящена всего одной галочке. Пора переходить к чему-то чуть более серьезному. Сегодняшняя тема — представление списков и связь GUI-списков с внутренними данными. Статья предназначена для Delphi-разработчиков.
С чего начать
Чтобы не лить воду, перейду сразу к живому примеру, приведенному на рисунке выше. Допустим, вам нужно создать примитивную форму настройки прав пользователей.
В левой части окна показывается список всех пользователей системы, а в правой — список прав и ролей текущего выбранного пользователя. Логика окна заключается в том, чтобы при выборе пользователя в левой части окна обновлялся список прав и ролей в правой части. Также в правой части есть кнопки «Добавить»/ «Удалить», позволяющие либо добавить пользователю новую роль или удалить выбранные существующие роли. При добавлении новых ролей появляется всплывающее окно справочника ролей, в котором можно выбрать добавляемые роли. Вот, собственно, и все.
Модель
Допустим, что внутреннее представление данных состоит из класса TUser, описывающего сотрудника, и справочника ролей, который умеет по числовому ID'у возвращать название роли. Заводить классы для ролей нецелесообразно, т.к. это слишком простая сущность:
uses Generics.Collections; // Чтобы можно было использовать типизированный TObjectList type TIntList = array of Integer; // Данный тип объявлен в отдельном общем модуле TUser = class strict private FID: Integer; FFullFio: String; FRoles: TIntList; public property ID: Integer read FID; property FullFio: String read FFullFio; property Roles: TIntList read FRoles write SetRoles; end; TUsersList = class(TObjectList<TUser>) public function UserByID(const aID: Integer): TUser; end;
Видно, что роли пользователя представлены крайне простым образом — списком ID'ов.
Добавляю соответствующие поля классу формы:
TfmUserRights = class(TForm) ... lbUsers: TListBox; lbRoles: TListBox; private FUsers: TUsersList; public property Users: TUsersList read FUsers; end;
Обратите внимание, что я использовал типизированный TObjectList. До Delphi 2009 такой возможности не было и TObjectList хранил всегда просто TObject'ы. При каждом обращении к элементу списка приходилось его приводить к корректному классу: FUsers[i] as TUser (ну или вариант для камикадзе: TUser(FUsers[i])). Это было неудобно и легко было допустить ошибку, выполнив преобразование не к тому классу. С появлением обобщенных типов (generics) теперь можно использовать жестко типизированный TObjectList. Это невероятно удобно! Обращаясь к элементам такого списка через FUsers[i] мы сразу получаем объект класса TUser.
Я не буду приводить код получения списка сотрудников, т.к. в каждой системе в зависимости от ее архитектуры он будет свой. Это может быть SQL-запрос к базе, обращение к какому-то клиентскому кэшу или обращение к серверу приложений (в многозвенной архитектуре). Предположим просто, что у вас есть возможность откуда-то получить этот список.
Отображение элементов списка
Итак, мы хотим получить список сотрудников и отобразить его на экране:
procedure TfmUserRights.FormCreate(Sender: TObject); begin FillUsers; end;
Метод Fill предназначен для простого [пере]заполнения списка пользователей:
procedure TfmUserRights.FillUsers; var i: Integer; begin FUsers.Free; // Удаляю старый спи��ок, если он был FUsers := GetUsers; lbUsers.Items.BeginUpdate; try lbUsers.Items.Clear; for i := 0 to Users.Count-1 do lbUsers.AddItem(FUsers[i].FullFio, FUsers[i]); // Добавляемый элемент списка сразу получает связь с объектом FUsers[i], // хотя в моем случае хватило бы и связи с ID'ами (позже вы увидите, почему) finally lbUsers.Items.EndUpdate; end; end;
Простого заполнения списка сотрудников недостаточно. Нужно еще показать роли текущего выбранного сотрудника. А для этого нужно научиться определять, какой сотрудник сейчас выбран? Неопытные программисты начинают активно обращаться из разных мест к lbUsers.Items.Objects[lbUsers.ItemIndex]. Однако, если вы читали предыдущую часть статьи, то уже догадываетесь, что мы пойдем другим путем. Мы заведем у класса формы свойство, возвращающее и устанавливающее текущего выбранного сотрудника. Возвращать можно либо сам объект TUser, либо числовой ID пользователя. Возвращать ID мне показалось удобнее, хотя с этим можно поспорить.
TfmUserRights = class(TForm) private FSelUserID: Integer; public property SelUserID: Integer read FSelUserID write SetSelUserID; end; procedure TfmUserRights.SetSelUserID(const Value: Integer); begin if FSelUserID <> Value then begin FSelUserID := Value; UpdateSelUser; // !!! end; end;
Ключевой момент здесь в методе UpdateSelUser, который приводит интерфейс в состояние, при котором выбран заданный пользователь:
procedure TfmUserRights.UpdateSelUser; var vSelInd: Integer; i: Integer; begin vSelInd := -1; with lbUsers do for i := 0 to Items.Count-1 do if (Items.Objects[i] as TUser).ID = SelUserID then begin vSelInd := i; Break; end; lbUsers.ItemIndex := vSelInd; if SelUserID <= 0 then gbRoles.Caption := 'Права и роли:' else gbRoles.Caption := 'Права и роли: ' + Users.UserByID(SelUserID).FullFio FillUserRoles; // !!! end;
Мы видим, что метод установки текущего пользователя всегда вызывает перезаполнение списка ролей (FillUserRoles).
Как и в предыдущей статье, раз мы реализовали направление синхронизации Модель->Представление, то нам нужна и обратная синхронизация. Поэтому в событии OnClick списка lbUsers добавим такой код:
procedure TfmUserRights.lbUsersClick(Sender: TObject); begin SelUserID := (lbUsers.Items.Objects[lbUsers.ItemIndex] as TUser).ID; end;
При задании SelUserID, если раньше был выбран другой пользователь, то set-метод вызовет UpdateSelUser, который в свою очередь полностью синхронизирует представление с моделью, а именно обновит список ролей. Т.е. мне уже не нужно вызывать метод обновления списка ролей изнутри обработчика lbUsersClick, все произойдет автоматически.
Приведу метод заполнения списка ролей (он тривиален):
procedure TfmUserRights.FillUserRoles; var i: Integer; vSelUser: TUser; begin lbRoles.BeginUpdate; try lbRoles.Clear; if SelUserID <= 0 then Exit; vSelUser := Users.UserByID(SelUserID); for i := 0 to High(vSelUser.Roles) do lbRoles.AddItem(DictRoles.NameByID(vSelUser.Roles[i]), TObject(vSelUser.Roles[i])); // Тут я сделал небольшую хитрость и привязал к элементам списка не объекты, а сами ID'ы, использовав приведение их к TObject'у (это допустимо) finally SomeList.EndUpdate; end; end;
Код инициализации формы я дополню инициализацией первого пользователя в списке:
procedure TfmUserRights.FormCreate(Sender: TObject); begin FillUsers; FSelUserID := -2; // Хочу, чтобы сработал Set-метод SelUserID := -1; // По умолчанию не выбираю никакого пользователя end;
Что мы получили? Теперь обращаться к текущему выбарнному пользователю можно через SelUserID. Причем как при программной установке значения свойства SelUserID, так и при выборе пользователя через GUI-список будет автоматически обновляться список ролей.
Для работы с ролями (добавление, удаление) можно завести у класса формыеще свойство SelRoles. Его проще сделать полностью виртуальным (не заводить для него отдельное поле):
property SelRoles: TIntList read GetSelRoles write SetSelRoles; function TfmUserRights.GetSelRoles: TIntList; var i: Integer; begin Result := nil; for i := 0 to lbRoles.Items.Count-1 do if lbRoles.Selected[i] then AddIntToList(Integer(lbRoles.Items.Objects[i]), Result); // Помните про вышеописанную хитрость? На самом деле в Objects'ах // сидят не объекты, а ID'ы ролей, поэтому смело привожу их к Integer end; procedure TfmReportMain.SetSelRoles(const aSelRoles: TIntList); var i: Integer; begin lbRoles.Items.BeginUpdate; try for i := 0 to lbRoles.Items.Count-1 do lbRoles.Selected[i] := IntInList(Integer(lbRoles.Items.Objects[i]), aSelRoles); finally lbRoles.Items.EndUpdate; end; UpdateSelRoles; // Этого метода может и не быть. В нем можно разместить код, к примеру, выводящий фразу "Выбрано N ролей" на статус баре или где-то еще end;
Методы IntInList и AddIntToList соответственно проверяют вхождение элемента в массив и добавляют новый элемент в массив.
Добавление и удаление ролей
Добавление ролей:
procedure TfmUserRights.btAddRoleClick(Sender: TObject); var vSelUser: TUser; vRoles: TIntList; vAddRoles: TIntList; i: Integer; begin vAddRoles := nil; vAddRoles := TfmDictionary.GelDictIDs(DictRoles); // Получаю список ID'ов выбранных ролей из всплывающего окна справочника vSelUser := Users.UserByID(SelUserID); vRoles := vSelUser.Roles; for i := 0 to High(vAddRoles) do AddIntToList(vAddRoles[i], vRoles); vSelUser.Roles := vRoles; // После добавления новых ролей сразу выделяю их в списке ролей (визуально это удобно) SelRoles := vAddRoles; end;
Удаление ролей:
procedure TfmUserRights.btDelRoleClick(Sender: TObject); var vSelUser: TUser; vDelRoles: TIntList; vRoles: TIntList; vNewRoles: TIntList; i, vInd: Integer; begin if lbAllowRightsRoles.SelCount = 0 then raise Exception.Create('Необходимо выделить в списке удаляемые роли.'); vDelRoles := SelRoles; vSelUser := Users.UserByID(SelUserID); vRoles := vSelUser.Roles; SetLength(vNewRoles, Length(vRoles)); // размер завожу про запас // В vNewRoles переношу только те роли, которые не входят в список удаляемых vInd := 0; for i := 0 to High(vRoles) do begin if IntInList(vRoles[i], vDelRoles) then Continue; vNewRoles[vInd] := vRoles[i]; inc(vInd); end; SetLength(vNewRoles, vInd); // усекаю до корректного размера vSelUser.Roles := vNewRoles; end;
В каком месте осуществлять сохранение изменений объекта TUser в БД решать вам. Кто-то, возможно, захочет делать это немедленно, прямо внутри SetRoles класса TUser (чтобы все изменения отражались в базе мгнове��но). Кто-то реализует сохранение измененных объектов TUser при нажатии на кнопку OK в окне. Третьим вариантом является сохранение по кнопке ОК, а также при попытке переключения между пользователями, если роли текущего пользователя были изменены (т.к. приведенный выше интерфейс окна не позволяет визуально отследить, у каких сотрудников роли поменялись, а у каких — нет, при переключении с одного сотрудника на другого, что может привести к ошибке).
Итог
Получилось окно управления правами пользователей. Окно реализует следующую логику:
1) Запрос списка сотрудников.
2) Отображение списка сотрудников.
3) Получение ID'а текущего выбранного сотрудника через SelUserID.
4) Установка выбранного сотрудника по ID'у с автоматическим обновлением списка его ролей.
5) Получение списка выбранных ролей сотрудника через SelRoles.
6) Добавление и удаление ролей.
Дополнение. Обновление списка с сохранением выбранного элемента
Здесь можно было бы и остановиться, но все-таки хочется показать, как можно обновлять и сам список сотрудников, не теряя при этом текущего выбранного сотрудника. Функциональность ручного обновления списка сотрудников может быть полезной, если добавление сотрудников производится через другое окно, а механизм автоматической нотификации окна изменения прав о добавлении нового сотрудника не реализован. Также нового сотрудника может добавить другой пользователь системы на другой машине, а вам не хочется перезаходить в окно настройки прав, чтобы добавленный пользователь появился в списке.
Итак, допустим вы до��авили еще кнопку «Обновить список сотрудников» в окно настройки прав. Очевидно, что она должна приводить к простому вызову метода FillUsers. Но ведь тогда текущий выбранный сотрудник потеряется (т.к. GUI список будет очищен и переазполнен заново), что будет очень неудобно и странно для пользователя.
procedure TfmUserRights.FillUsers; var i: Integer; vSavedSelUserID: Integer; begin // Перед перестроением списка запоминаю текущего выбранного пользователя if SelUserID > 0 then vSavedSelUserID := SelUserID else vSavedSelUserID := -1; ... // переполучаю данные FUsers и перезаполняю список ... // Устанавливаю заново текущего пользователя if vSavedSelUserID > 0 then begin FSelUserID := -1; SelUserID := vSavedSelUserID; end else SelUserID := -1; end;
В дальнейшем может потребоваться еще большее: запоминать последнего выбранного сотрудника между повторными входами в окно настройки прав или даже между сеансами работы приложения. В этом случае в FillUsers можно добавить параметр, определяющий, на каком пользователе нужно спозиционироваться после перестроения списка. При этом логику запоминания текущего пользователя придется немного усложнить:
procedure TfmUserRights.FillUsers(const aSelUserID: Integer = -1); var i: Integer; vNeedSelUserID: Integer; begin if aSelUserID > 0 then // Если передано на ком позиционироваться, то на нем vNeedSelUserID := aSelUserID else if SelUserID > 0 then // Иначе если выбран текущий - то на текущем vNeedSelUserID := SelUserID else vNeedSelUserID := -1; ... // переполучаю данные FUsers и перезаполняю список ... if vNeedSelUserID > 0 then begin FSelUserID := -1; SelUserID := vNeedSelUserID; end else SelUserID := -1; end;
При этом FormCreate поменяется на
procedure TfmUserRights.FormCreate(Sender: TObject); begin FillUsers(Config.RightsFormSavedUserID); end;
а FormDestroy на
procedure TfmUserRights.FormCreate(Sender: TObject); begin Config.RightsFormSavedUserID := SelUserID; end;
Большая часть вышеприведенного кода придумана из головы, не судите слишком строго за опечатки и неточности. Он очень похож на реальный проект, но на самом деле в реальном проекте гораздо больше деталей, о которых сейчас говорить не хочется.
Постепенно я все ближе подхожу к тому, чтобы связать с GUI-контролами сами классы внутренних данных. Пока еще я этого не сделал. В следующей части статьи я рассмотрю шаблон подписки на уведомления и покажу, как GUI-интерфейс может реагировать на изменения самих объектов.
Удачи!
PS
1-я часть статьи
3-я часть статьи