태블릿용 멀티컬럼 리스트뷰 ItemAppearace 만들기

2015.02.09 11:28

모바일(폰과 패드)용 목록을 만드는 컴포넌트는 대표적으로 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 속성에 새로운 항목이 추가된 것을 볼 수 있습니다.

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.



저작자 표시 비영리 동일 조건 변경 허락
신고

험프리.김현수 파이어몽키

  1. Blog Icon
    고재학

    소스 코드에 버그가 있는것 같네요.
    230 Text.InternalPlaceOffset.X :=
    231 Image.ActualPlaceOffset.X + Image.ActualWidth + LImageTextPlaceOffset;


  2. 어떤 증상이 있죠? 좀 더 자세히 알려주시면 감사하겠습니다.^^

  3. Blog Icon
    고재학

    윈도우에서 테스트할 경우에는 문제가 없었는데 모바일에서 컴파일하면 내용을 표시하는 과정에서 "Invalid floating point operation"가 나타납니다.
    위 코드를 제거하고 테스트해보니 정상작동 하였습니다.

  4. Blog Icon
    고재학

    질문에 답변도 해주시는지요?
    위의 예제를 응용하여 리스트뷰를 만들었는데... MultiDetail1에 대입되는 값에 따라 글자색을 변경하고 싶습니다.

    예를 들어
    MultiDetail1의 값이 1이면 MultiDetail1의 글자색은 빨강색
    MultiDetail1의 값이 2이면 MultiDetail1의 글자색은 파랑색

    모바일앱에서 MultiDetail1의 TextColor를 어떻게 동적으로 변경할 수 있을까요?

  5. C:\Users\Public\Documents\Embarcadero\Studio\17.0\Samples\Object Pascal\Multi-Device Samples\User Interface\ListView\ListViewMultiDetailAppearance

    위 ListView 샘플에 다음 코드로 진행해 봤습니다.(잘되네요^^)

    // ListView의 OnUpdateObjects 이벤트에 아래 코드 구현
    procedure TForm594.ListViewMultiDetailUpdateObjects(const Sender: TObject;
    const AItem: TListViewItem);
    var
    Textitem: TListItemText;
    begin
    Textitem := AItem.View.FindDrawable(TMultiDetailAppearanceNames.Detail1) as TListItemText;
    if Assigned(TextItem) then
    begin
    // M으로 시작하는 text 빨간색으로 표시
    if Textitem.Text.StartsWith('M') then
    begin
    TextItem.TextColor := $FFFF0000;
    end;
    end;
    end;

    위 디렉토리의 샘플을 참고하시면 다양한 기능을 구현할 수 있습니다.

  6. Blog Icon
    고재학

    와~~~ 멋지네요.
    XE8에서 작동하지않아 Seattle로 업그레이드하니 잘 됩니다.
    감사합니다.