Предыдущая статья была посвящена всего одной галочке. Пора переходить к чему-то чуть более серьезному. Сегодняшняя тема — представление списков и связь 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
FUsers := GetUserList;
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-интерфейс может реагировать на изменения самих объектов.
Удачи!
Автор: alan008