모바일(폰과 패드)용 목록을 만드는 컴포넌트는 대표적으로 ListView와 ListBox가 있습니다.
두 목록 컴포넌트의 차이점은 이름으로 알수 있듯이 목적에 차이가 있습니다.
ListView는
View 즉 보여주는 것을 목적으로 하기 때문에 목록을 빠르게 이동할 수 있지만 목록아이템을 꾸미는데 제한적입니다.
반면,
ListBox는
Box 즉 목록 아이템에 다른 아이템을 담아 자유롭게 목록을 구성할 수 있는 컴포넌트입니다. 목록을 원하는데로 꾸밀 수 있지만 많은 컴포넌트를 담는다면 스크롤이 상대적으로 느려질 수 있습니다.
두 목록 컴포넌트의 목적을 잘 이해하고 사용하시기 바랍니다.
ListView는 목록 아이템을 꾸미는데 제한적이라고 했는데요. 그 이유는 TListViewItem은 (TFMXObject를 상속받지 않았기 때문에)다른 컴포넌트를 올릴 수 없도록 설계되었습니다. 아이템 외관을 꾸미기 위해서는 ListView의 ItemAppearance 속성을 이용할 수 있습니다. 이 속성은 기본으로 7개의 항목만 제공됩니다.
기본 제공되는 ItemAppearance는 모바일 폰(작은화면)을 기준으로 제공합니다. 태블릿용 앱에서는 화면이 다소 허전해 질 수 있습니다.
이렇게 기본 제공되는 외관을 변경(항목 추가, 위치 이동)하기 위해서는 ItemAppearance 패키지 프로젝트를 직접 만들어 설치(Install) 후 사용할 수 있습니다.
ItemAppearance 패키지 제작
기본 샘플 참고
델파이 기본 샘플에서 ListView 샘플(Samples\Object Pascal\Mobile Samples\User Interface\ListView)을 참고할 수 있습니다.
ListView 샘플에는 상세정보를 여러건 보여주는 MultiDetailItem, 별점을 표시하는 RatingItem 등의 목록을 구성할 수 있는 패키지 프로젝트(*.dpk)가 있고, 그 패키지를 사용하는 샘플 프로젝트가 있습니다.
먼저 패키지 프로젝트를 열고 프로젝트 매니저에서 설치(오른쪽 마우스 > Install) 후 샘플 프로젝트를 열어 확인하기 바랍니다.
패키지 프로젝트를 열면 ItemAppearance 속성에 새로운 항목이 추가된 것을 볼 수 있습니다.
- 엠바카데로 문서 - Customizing FireMonkey ListView Appearance
MultiDetailHorzItem
제가 직접 4개의 열을 갖는 ItemAppearance를 만들어 테스트 해 봤습니다. 기본 제공되는 MultiDetailItem을 수정했습니다.
확실히 ListBox, Grid, StringGrid에 비해 목록을 스크롤하는 속도가 빠릅니다.
unit MultiDetailHorzAppearanceU; interface uses FMX.ListView, FMX.ListView.Types, System.Classes, System.SysUtils, FMX.Types, System.UITypes, FMX.MobilePreview; type TMultiDetailHorzAppearanceNames = class public const ListItem = 'MultiDetailHorzItem'; ListItemCheck = ListItem + 'ShowCheck'; ListItemDelete = ListItem + 'Delete'; Detail1 = 'det1'; // Name of MultiDetail object/data Detail2 = 'det2'; Detail3 = 'det3'; end; implementation uses System.Math, System.Rtti; type TMultiDetailHorzItemAppearance = class(TPresetItemObjects) public const cTextMarginAccessory = 8; cDefaultHeight = 40; private FMultiDetail1: TTextObjectAppearance; FMultiDetail2: TTextObjectAppearance; FMultiDetail3: TTextObjectAppearance; procedure SetMultiDetail1(const Value: TTextObjectAppearance); procedure SetMultiDetail2(const Value: TTextObjectAppearance); procedure SetMultiDetail3(const Value: TTextObjectAppearance); protected function DefaultHeight: Integer; override; procedure UpdateSizes; override; function GetGroupClass: TPresetItemObjects.TGroupClass; override; procedure SetObjectData(const AListViewItem: TListViewItem; const AIndex: string; const AValue: TValue; var AHandled: Boolean); override; public constructor Create; override; destructor Destroy; override; published property MultiDetail1: TTextObjectAppearance read FMultiDetail1 write SetMultiDetail1; property MultiDetail2: TTextObjectAppearance read FMultiDetail2 write SetMultiDetail2; property MultiDetail3: TTextObjectAppearance read FMultiDetail3 write SetMultiDetail3; property Accessory; end; TMultiDetailHorzDeleteAppearance = class(TMultiDetailHorzItemAppearance) private const cDefaultGlyph = TGlyphButtonType.Delete; public constructor Create; override; published property GlyphButton; end; TMultiDetailShowCheckAppearance = class(TMultiDetailHorzItemAppearance) private const cDefaultGlyph = TGlyphButtonType.Checkbox; public constructor Create; override; published property GlyphButton; end; const cMultiDetail1Member = 'Detail1'; cMultiDetail2Member = 'Detail2'; cMultiDetail3Member = 'Detail3'; constructor TMultiDetailHorzItemAppearance.Create; begin inherited; Accessory.DefaultValues.AccessoryType := TAccessoryType.More; Accessory.DefaultValues.Visible := True; Accessory.RestoreDefaults; Text.DefaultValues.VertAlign := TListItemAlign.Trailing; Text.DefaultValues.TextVertAlign := TTextAlign.Center; Text.DefaultValues.Visible := True; Text.RestoreDefaults; FMultiDetail1 := TTextObjectAppearance.Create; FMultiDetail1.Name := TMultiDetailHorzAppearanceNames.Detail1; FMultiDetail1.DefaultValues.Assign(Text.DefaultValues); // Start with same defaults as Text object FMultiDetail1.DefaultValues.IsDetailText := True; // Use detail font FMultiDetail1.VertAlign := TListItemAlign.Leading; FMultiDetail1.Align := TListItemAlign.Trailing; FMultiDetail1.TextVertAlign := TTextAlign.Center; FMultiDetail1.RestoreDefaults; FMultiDetail1.OnChange := Self.ItemPropertyChange; FMultiDetail1.Owner := Self; FMultiDetail2 := TTextObjectAppearance.Create; FMultiDetail2.Name := TMultiDetailHorzAppearanceNames.Detail2; FMultiDetail2.DefaultValues.Assign(FMultiDetail1.DefaultValues); // Start with same defaults as Text object FMultiDetail2.VertAlign := TListItemAlign.Leading; FMultiDetail2.Align := TListItemAlign.Trailing; FMultiDetail2.TextVertAlign := TTextAlign.Center; FMultiDetail2.RestoreDefaults; FMultiDetail2.OnChange := Self.ItemPropertyChange; FMultiDetail2.Owner := Self; FMultiDetail3 := TTextObjectAppearance.Create; FMultiDetail3.Name := TMultiDetailHorzAppearanceNames.Detail3; FMultiDetail3.DefaultValues.Assign(FMultiDetail2.DefaultValues); // Start with same defaults as Text object // FMultiDetail3.DefaultValues.Height := 20; // Move text down FMultiDetail3.VertAlign := TListItemAlign.Leading; FMultiDetail3.Align := TListItemAlign.Trailing; FMultiDetail3.TextVertAlign := TTextAlign.Center; FMultiDetail3.RestoreDefaults; FMultiDetail3.OnChange := Self.ItemPropertyChange; FMultiDetail3.Owner := Self; // Define livebindings members that make up MultiDetail FMultiDetail1.DataMembers := TObjectAppearance.TDataMembers.Create( TObjectAppearance.TDataMember.Create( cMultiDetail1Member, // Displayed by LiveBindings Format('Data["%s"]', [TMultiDetailHorzAppearanceNames.Detail1]))); // Expression to access value from TListViewItem FMultiDetail2.DataMembers := TObjectAppearance.TDataMembers.Create( TObjectAppearance.TDataMember.Create( cMultiDetail2Member, // Displayed by LiveBindings Format('Data["%s"]', [TMultiDetailHorzAppearanceNames.Detail2]))); // Expression to access value from TListViewItem FMultiDetail3.DataMembers := TObjectAppearance.TDataMembers.Create( TObjectAppearance.TDataMember.Create( cMultiDetail3Member, // Displayed by LiveBindings Format('Data["%s"]', [TMultiDetailHorzAppearanceNames.Detail3]))); // Expression to access value from TListViewItem GlyphButton.DefaultValues.VertAlign := TListItemAlign.Center; GlyphButton.RestoreDefaults; // Define the appearance objects AddObject(Text, True); AddObject(MultiDetail1, True); AddObject(MultiDetail2, True); AddObject(MultiDetail3, True); AddObject(Image, True); AddObject(Accessory, True); AddObject(GlyphButton, IsItemEdit); // GlyphButton is only visible when in edit mode end; constructor TMultiDetailHorzDeleteAppearance.Create; begin inherited; GlyphButton.DefaultValues.ButtonType := cDefaultGlyph; GlyphButton.DefaultValues.Visible := True; GlyphButton.RestoreDefaults; end; constructor TMultiDetailShowCheckAppearance.Create; begin inherited; GlyphButton.DefaultValues.ButtonType := cDefaultGlyph; GlyphButton.DefaultValues.Visible := True; GlyphButton.RestoreDefaults; end; function TMultiDetailHorzItemAppearance.DefaultHeight: Integer; begin Result := cDefaultHeight; end; destructor TMultiDetailHorzItemAppearance.Destroy; begin FMultiDetail1.Free; FMultiDetail2.Free; FMultiDetail3.Free; inherited; end; procedure TMultiDetailHorzItemAppearance.SetMultiDetail1( const Value: TTextObjectAppearance); begin FMultiDetail1.Assign(Value); end; procedure TMultiDetailHorzItemAppearance.SetMultiDetail2( const Value: TTextObjectAppearance); begin FMultiDetail2.Assign(Value); end; procedure TMultiDetailHorzItemAppearance.SetMultiDetail3( const Value: TTextObjectAppearance); begin FMultiDetail3.Assign(Value); end; procedure TMultiDetailHorzItemAppearance.SetObjectData( const AListViewItem: TListViewItem; const AIndex: string; const AValue: TValue; var AHandled: Boolean); begin inherited; end; function TMultiDetailHorzItemAppearance.GetGroupClass: TPresetItemObjects.TGroupClass; begin Result := TMultiDetailHorzItemAppearance; end; procedure TMultiDetailHorzItemAppearance.UpdateSizes; const // Total Rate = 1.0 TextWidthRate = 0.4; Det1WidthRate = 0.2; Det2WidthRate = 0.2; Det3WidthRate = 0.2; var LOuterHeight: Single; LOuterWidth: Single; LInternalWidth: Single; LImagePlaceOffset: Single; LImageTextPlaceOffset: Single; begin BeginUpdate; try inherited; // Update the widths and positions of renderening objects within a TListViewItem LOuterHeight := Height - Owner.ItemSpaces.Top - Owner.ItemSpaces.Bottom; LOuterWidth := Owner.Width - Owner.ItemSpaces.Left - Owner.ItemSpaces.Right; Text.InternalPlaceOffset.X := Image.ActualPlaceOffset.X + Image.ActualWidth + LImageTextPlaceOffset; LInternalWidth := (LOuterWidth - Text.ActualPlaceOffset.X - Accessory.ActualWidth); if Accessory.ActualWidth > 0 then LInternalWidth := LInternalWidth - cTextMarginAccessory; Text.InternalWidth := Max(1, LInternalWidth * TextWidthRate); MultiDetail1.InternalWidth := LInternalWidth * Det1WidthRate; MultiDetail1.InternalPlaceOffset.X := Text.InternalPlaceOffset.X + Text.InternalWidth; MultiDetail2.InternalWidth := LInternalWidth * Det2WidthRate; MultiDetail2.InternalPlaceOffset.X := MultiDetail1.InternalPlaceOffset.X + MultiDetail1.InternalWidth; MultiDetail3.InternalWidth := LInternalWidth * Det3WidthRate; MultiDetail3.InternalPlaceOffset.X := MultiDetail2.InternalPlaceOffset.X + MultiDetail2.InternalWidth; finally EndUpdate; end; end; type TOption = TCustomListView.TRegisterAppearanceOption; const sThisUnit = 'MultiDetailHorzAppearanceU'; // Will be added to the uses list when appearance is used initialization // MultiDetailItem group TCustomListView.RegisterAppearance( TMultiDetailHorzItemAppearance, TMultiDetailHorzAppearanceNames.ListItem, [TOption.Item], sThisUnit); TCustomListView.RegisterAppearance( TMultiDetailHorzDeleteAppearance, TMultiDetailHorzAppearanceNames.ListItemDelete, [TOption.ItemEdit], sThisUnit); TCustomListView.RegisterAppearance( TMultiDetailShowCheckAppearance, TMultiDetailHorzAppearanceNames.ListItemCheck, [TOption.ItemEdit], sThisUnit); finalization TCustomListView.UnregisterAppearances( TArray.Create( TMultiDetailHorzItemAppearance, TMultiDetailHorzDeleteAppearance, TMultiDetailShowCheckAppearance)); end.