Лучший подход для хранения, извлечения, сохранения и загрузки данных в TListBox?

avatar
user1580348
8 августа 2021 в 16:10
164
1
1

В приложении Delphi 10.4.2 win-32 VCL в Windows 10 я использую простой TListBox для хранения записей в элементах ListBox. Когда пользователь щелкает элемент, извлекаются данные связанной записи:

type
  PResizeSettingsRec = ^TResizeSettingsRec;
  TResizeSettingsRec = record
    TopValue:    Integer;
    LeftValue:   Integer;
    RightValue:  Integer;
    BottomValue: Integer;
    Color:       TColor;
    Opacity:     Integer;
  end;

procedure TformMain.btnAddCurrentSettingsClick(Sender: TObject);
var
  P: PResizeSettingsRec;
begin
  New(P);
  P.TopValue    := 5;
  P.LeftValue   := 5;
  P.RightValue  := 5;
  P.BottomValue := 5;
  P.Color       := clRed;
  P.Opacity     := 255;
  listboxMultiResize.AddItem('Step 1', TObject(P));

  New(P);
  P.TopValue    := 9;
  P.LeftValue   := 9;
  P.RightValue  := 9;
  P.BottomValue := 9;
  P.Color       := clBlue;
  P.Opacity     := 127;
  listboxMultiResize.AddItem('Step 2', TObject(P));
end;

procedure TformMain.FormDestroy(Sender: TObject);
begin
  for var i := 0 to listboxMultiResize.Items.Count - 1 do
    Dispose(PResizeSettingsRec(listboxMultiResize.Items.Objects[i]));
end;

procedure TformMain.listboxMultiResizeClick(Sender: TObject);
var
  P: PResizeSettingsRec;
begin
  if listboxMultiResize.ItemIndex < 0 then EXIT;
  P := PResizeSettingsRec(listboxMultiResize.Items.Objects[listboxMultiResize.ItemIndex]);
  CodeSite.Send('TformMain.listboxMultiResizeClick: P.TopValue', P.TopValue);
  CodeSite.Send('TformMain.listboxMultiResizeClick: P.LeftValue', P.LeftValue);
  CodeSite.Send('TformMain.listboxMultiResizeClick: P.RightValue', P.RightValue);
  CodeSite.Send('TformMain.listboxMultiResizeClick: P.BottomValue', P.BottomValue);
  CodeSite.Send('TformMain.listboxMultiResizeClick: ColorToString(P.Color)', ColorToString(P.Color));
  CodeSite.Send('TformMain.listboxMultiResizeClick: P.Opacity', P.Opacity);
end;

Этот подход намеренно упрощен, чтобы сделать его стабильным и надежным. Или есть лучший и более современный и, следовательно, более простой способ добиться этого?

Есть ли простой способ сохранить элементы ListBox вместе с их данными в файл, а затем перезагрузить их из файла? (В настоящее время я использую файл INI).

EDIT: Следуя совету Андреаса, я теперь использую этот код:

type
  TResizeSettings = class
    TopValue:    Integer;
    LeftValue:   Integer;
    RightValue:  Integer;
    BottomValue: Integer;
    Color:       TColor;
    Opacity:     Integer;
  end;

procedure TformMain.btnAddCurrentSettingsClick(Sender: TObject);
var
  ThisResizeSettings: TResizeSettings;
begin
  ThisResizeSettings := TResizeSettings.Create;
  try
    ThisResizeSettings.TopValue    := 5;
    ThisResizeSettings.LeftValue   := 5;
    ThisResizeSettings.RightValue  := 5;
    ThisResizeSettings.BottomValue := 5;
    ThisResizeSettings.Color       := clRed;
    ThisResizeSettings.Opacity     := 255;
    listboxMultiResize.AddItem('Step 1', ThisResizeSettings);
  finally
    ThisResizeSettings.Free;
  end;

  ThisResizeSettings := TResizeSettings.Create;
  try
    ThisResizeSettings.TopValue    := 9;
    ThisResizeSettings.LeftValue   := 9;
    ThisResizeSettings.RightValue  := 9;
    ThisResizeSettings.BottomValue := 9;
    ThisResizeSettings.Color       := clBlue;
    ThisResizeSettings.Opacity     := 127;
    listboxMultiResize.AddItem('Step 2', ThisResizeSettings);
  finally
    ThisResizeSettings.Free;
  end;
end;

procedure TformMain.FormDestroy(Sender: TObject);
begin
  for var i := 0 to listboxMultiResize.Items.Count - 1 do
  begin
    TResizeSettings(listboxMultiResize.Items.Objects[i]).Free;
  end;
end;

procedure TformMain.listboxMultiResizeClick(Sender: TObject);
var
  ThisResizeSettings: TResizeSettings;
begin
  if listboxMultiResize.ItemIndex < 0 then EXIT;
  ThisResizeSettings := TResizeSettings(listboxMultiResize.Items.Objects[listboxMultiResize.ItemIndex]);
  CodeSite.Send('TformMain.listboxMultiResizeClick: TopValue', ThisResizeSettings.TopValue);
  CodeSite.Send('TformMain.listboxMultiResizeClick: LeftValue', ThisResizeSettings.LeftValue);
  CodeSite.Send('TformMain.listboxMultiResizeClick: RightValue', ThisResizeSettings.RightValue);
  CodeSite.Send('TformMain.listboxMultiResizeClick: BottomValue', ThisResizeSettings.BottomValue);
  CodeSite.Send('TformMain.listboxMultiResizeClick: ColorToString(P.Color)', ColorToString(ThisResizeSettings.Color));
  CodeSite.Send('TformMain.listboxMultiResizeClick: Opacity', ThisResizeSettings.Opacity);
end;

Однако я получаю случайные результаты при нажатии на элемент Listbox!

EDIT2: После ответа Андреаса я больше не освобождаю созданные объекты:

procedure TformMain.btnAddCurrentSettingsClick(Sender: TObject);
// https://coderhelper.com/questions/68702539/better-approach-for-storing-retrieving-saving-and-loading-data-in-a-tlistbox
var
  ThisResizeSettings: TResizeSettings;
begin
  ThisResizeSettings := TResizeSettings.Create;  
    ThisResizeSettings.TopValue    := 5;
    ThisResizeSettings.LeftValue   := 5;
    ThisResizeSettings.RightValue  := 5;
    ThisResizeSettings.BottomValue := 5;
    ThisResizeSettings.Color       := clRed;
    ThisResizeSettings.Opacity     := 255;
    listboxMultiResize.AddItem('Step 1', ThisResizeSettings);   

  ThisResizeSettings := TResizeSettings.Create;  
    ThisResizeSettings.TopValue    := 9;
    ThisResizeSettings.LeftValue   := 9;
    ThisResizeSettings.RightValue  := 9;
    ThisResizeSettings.BottomValue := 9;
    ThisResizeSettings.Color       := clBlue;
    ThisResizeSettings.Opacity     := 127;
    listboxMultiResize.AddItem('Step 2', ThisResizeSettings);  
end;

Я также изменил освобождение объектов ListBox в FormDestroy:

procedure TformMain.FormDestroy(Sender: TObject);
begin
  // https://coderhelper.com/questions/68702539/better-approach-for-storing-retrieving-saving-and-loading-data-in-a-tlistbox
  for var i := listboxMultiResize.Items.Count - 1 downto 0 do
  begin
    listboxMultiResize.Items.Objects[i].Free;
    listboxMultiResize.Items.Objects[i] := nil;
  end;

Теперь вроде работает. Спасибо, Андреас!

Источник
Andreas Rejbrand
8 августа 2021 в 16:22
0

Вместо записей используйте объекты. Тогда уродливое приведение типа TObject не нужно, потому что вы фактически предоставляете объект! Вот как это (в основном) предполагается использовать.

user1580348
8 августа 2021 в 16:25
0

@AndreasRejbrand Вы имеете в виду, что я должен объявить класс с сеттерами и геттерами вместо записи? Разве с записью не проще обращаться? Разве класс не добавляет больше ненужной сложности?

Andreas Rejbrand
8 августа 2021 в 16:27
1

Классу не обязательно иметь геттеры и сеттеры. Вместо type TTest = record A, B: Integer; c: string; end; просто введите type TTest = class A, B: Integer; c: string; end;. По умолчанию A и B и c будут общедоступными. Нет необходимости в частных полях и общедоступных свойствах с геттерами и сеттерами, если они вам не нужны. Да, записи — это типы-значения, а объекты — это ссылочные типы (и их нужно создавать и освобождать), но в вашем случае удобнее со ссылочными типами. Кроме того, вы уже занимаетесь выделением и освобождением, пусть и древним способом!

user1580348
8 августа 2021 в 18:30
0

@AndreasRejbrand Я последовал твоему совету использовать класс вместо записи. Но когда я нажимаю на элемент в ListBox, я получаю в основном случайные значения с этим кодом: ThisResizeSettings := TResizeSettings(listboxMultiResize.Items.Objects[listboxMultiResize.ItemIndex]); CodeSite.Send('TformMain.listboxMultiResizeClick: TopValue', ThisResizeSettings.TopValue);

Andreas Rejbrand
8 августа 2021 в 18:35
0

Тогда вы делаете что-то не так. Для начала используйте не небезопасные приведения, а безопасные as приведения.

user1580348
8 августа 2021 в 18:40
0

@AndreasRejbrand Я разместил измененный код в вопросе. Что не так с этим кодом?

Andreas Rejbrand
8 августа 2021 в 18:41
0

Ваш новый код неверен. Помните, что ThisResizeSettings := TResizeSettings.Create сначала создает новый объект TResizeSettings в куче, а затем сохраняет адрес этого объекта в локальной переменной ThisResizeSettings. Затем listboxMultiResize.AddItem('Step 1', ThisResizeSettings); добавляет новый элемент списка с текстом Step 1 и этим адресом. Но тогда ThisResizeSettings.Free уничтожает объект кучи, так что теперь адрес списка представляет собой висячий указатель, то есть указатель, указывающий на мусор!

Andreas Rejbrand
8 августа 2021 в 18:42
0

(мусор = неиспользуемая память, которая может быть использована другими вещами в будущем, и в этом случае вы можете вызвать повреждение памяти, притворившись, что это все еще ваш бывший объект TResizeSettings)

Andreas Rejbrand
8 августа 2021 в 18:42
0

Создаваемый вами объект должен существовать до тех пор, пока на него существует >= 1 ссылка, включая ссылку на список.

user1580348
8 августа 2021 в 18:50
0

Итак, вы говорите, что я не должен использовать ThisResizeSettings.Free при добавлении элемента в ListBox? Я думал, что при добавлении элемента ListBox этот элемент получает копию созданного объекта.

Andreas Rejbrand
8 августа 2021 в 18:51
0

Нет. Это все равно, что (1) построить новый дом для ваших старых родителей, (2) дать им адрес и сказать, что они могут переехать в новый дом в любое время, а затем (3) немедленно снести дом (никем не сообщая об этом). это!) еще до того, как они увидят его в первый раз! (10) Когда они идут по адресу через месяц, они не знают, что этот район теперь используется ЦРУ, и сотрудники ЦРУ очень злятся на них, когда они входят в этот район.

Andreas Rejbrand
8 августа 2021 в 19:00
0

"Я думал, что при добавлении элемента ListBox этот элемент получает копию созданного объекта". положение в оперативной памяти вашего компьютера ("куча"). Очень важно: Frog := TFrog.Create (1) выделяет память в куче для нового объекта TFrog и (2) запускает конструктор TFrog.Create для этого указателя. (3) Затем адрес этого объекта (64-битное целое число, указатель) сохраняется в переменной Frog. Думайте о Frog как об адресе, который указывает на конкретный...

Andreas Rejbrand
8 августа 2021 в 19:03
0

... область памяти вашего компьютера. Если вы введете Frog2 := Frog, то и Frog, и Frog2 будут указывать на одно и то же место (например, Frog = Frog2 = 45012112). Если вы уничтожите этот объект TFrog в куче (например, выполнив Frog.Free или Frog2.Free или что-то еще), вы должны убедиться, что вы после этого понимаете, что Frog и Frog2 теперь висячие указатели, то есть указатели, указывающие на что-то, чем вы больше не владеете или не знаете, что это такое. Вы никогда больше не должны использовать эти указатели.

Ответы (1)

avatar
Andreas Rejbrand
8 августа 2021 в 18:55
1

Вместо записей используйте объекты. Тогда уродливое приведение TObject не понадобится, потому что вы на самом деле предоставляете объект! Вот как это (в основном) предполагается использовать.

Помните: классу не обязательно иметь геттеры и сеттеры, и по умолчанию класс на основе TObject имеет видимость членов public. Нет необходимости в частных полях и общедоступных свойствах с геттерами и сеттерами, если они вам не нужны. Кроме того, записи являются типами значений, а объекты — ссылочными типами (и их нужно создавать и освобождать), но в вашем случае удобнее со ссылочными типами. Кроме того, вы уже занимаетесь выделением и освобождением, пусть и древним способом!

Вот простой пример:

type
  TPerson = class
    Name: string;
    Age: Integer;
  end;

  TForm1 = class(TForm)
    lbPersons: TListBox;
    eName: TEdit;
    lblName: TLabel;
    eAge: TEdit;
    lblAge: TLabel;
    btnAddUpd: TButton;
    procedure FormDestroy(Sender: TObject);
    procedure btnAddUpdClick(Sender: TObject);
    procedure lbPersonsClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

implementation

{$R *.dfm}

procedure TForm1.btnAddUpdClick(Sender: TObject);
begin
  var idx := lbPersons.Items.IndexOf(eName.Text);
  var Person: TPerson;
  if idx <> -1 then
    Person := lbPersons.Items.Objects[idx] as TPerson
  else
    Person := TPerson.Create;
  Person.Name := eName.Text;
  Person.Age := StrToInt(eAge.Text);
  if idx = -1 then
    lbPersons.Items.AddObject(Person.Name, Person);
  if eName.CanFocus then
    eName.SetFocus;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  for var i := lbPersons.Items.Count - 1 downto 0 do
  begin
    lbPersons.Items.Objects[i].Free;
    lbPersons.Items.Objects[i] := nil;
  end;
end;

procedure TForm1.lbPersonsClick(Sender: TObject);
begin
  if lbPersons.ItemIndex = -1 then
  begin
    eName.Text := '';
    eAge.Text := '';
  end
  else
  begin
    var Person := lbPersons.Items.Objects[lbPersons.ItemIndex] as TPerson;
    eName.Text := Person.Name;
    eAge.Text := Person.Age.ToString;
  end;
end;

Screenshot of test app.

Delphi Coder
10 августа 2021 в 07:29
0

Разве не опубликована видимость по умолчанию!?

Andreas Rejbrand
10 августа 2021 в 07:35
0

@DelphiCoder: Нет, это public, если только директива {$M+} не использовалась до объявления класса или класс-предок не имеет членов published по умолчанию. Таким образом, потомки TPersistent, которые включают в себя все компоненты (и, следовательно, все элементы управления), имеют published в качестве видимости по умолчанию. Но TObject не имеет этого состояния, поэтому мое утверждение «класс на основе TObject имеет общедоступную видимость членов» верно. См. docwiki.embarcadero.com/RADStudio/Sydney/en/….