Naciśnij “Enter” aby skoczyć do treści

Tiny Radio Player #10 – Lista stacji radiowych, konfiguracja VirtualTreeView

Do prezentacji stacji radiowych użyjemy komponentu VirtualTreeView. Jego główną zaletą jest szybkość. Dodanie miliona węzłów (nodów) zajmuje mniej niż jedną sekundę. W naszym przypadku to aż nadto bo nie przewidujemy takiej ilości danych. Nie będziemy mieli jednak problemu z szybką aktualizacją informacji podczas edycji lub wyszukiwania stacji radiowych.

VirtualTreeView ma ogromne możliwości, o których przeczytasz na stronie soft-gems.net. My skorzystamy tylko z podstawowych funkcji potrzebnych do wyświetlenia Grida z wierszami podzielonymi na kilka kolumn.

Opis instalacji komponentu VirtualTreeView znajdziesz tu.

Konfiguracja

Aby zwiększyć przejrzystość, instancję klasy VirtualStringTree utworzymy w sposób dynamiczny.

TMainForm = class(TForm)
private
  procedure CreateVstStationList;
public
  VstStationList: TVirtualStringTree;
end;

implementation

procedure TMainForm.CreateVstStationList;
begin
  VstStationList := TVirtualStringTree.Create(Self);

  VstStationList.Name := 'StationList';
  VstStationList.Parent := MainPanel;

  VstStationList.Align := alCustom;

  VstStationList.DefaultNodeHeight := 20;
  VstStationList.SelectionCurveRadius := 0;
  VstStationList.TabOrder := 0;

  VstStationList.BorderStyle := bsNone;

  // Events
  VstStationList.OnBeforeItemErase := @VstStationListBeforeItemErase;
  VstStationList.OnHeaderClick := @VstStationListHeaderClick;
  VstStationList.OnFreeNode := @VstStationListFreeNode;
  VstStationList.OnCompareNodes := @VstStationListCompareNodes;
  VstStationList.OnGetText := @VstStationListGetText;
  VstStationList.OnDblClick := @VstStationListDblClick;
  VstStationList.OnKeyPress := @VstStationListKeyPress;
  VstStationList.OnFocusChanged := @VstStationListFocusChanged;
  VstStationList.OnGetNodeDataSize := @VstStationListGetNodeDataSize;
  VstStationList.OnAfterColumnWidthTracking := @VstStationListAfterColumnWidthTracking;
  VstStationList.OnHeaderDrawQueryElements := @VstStationListHeaderDrawQueryElements;
  VstStationList.OnAdvancedHeaderDraw := @VstStationListAdvancedHeaderDraw;

  // Columns
  VstStationList.Header.Columns.Add.Text := 'Station Name';
  VstStationList.Header.Columns[0].Width := 240;
  VstStationList.Header.Columns.Add.Text := 'Genre';
  VstStationList.Header.Columns[1].Width := 120;
  VstStationList.Header.Columns.Add.Text := 'Country';
  VstStationList.Header.Columns[2].Width := 90;

  // Visibility of the header columns
  VstStationList.Header.Options := [hoVisible, hoColumnResize, hoHotTrack, hoOwnerDraw, hoShowHint, hoShowImages, hoShowSortGlyphs];
  VstStationList.Header.Height := 20;

  // Options
  VstStationList.TreeOptions.AnimationOptions := [toAnimatedToggle];
  VstStationList.TreeOptions.AutoOptions := [toAutoDropExpand, toAutoScroll, toAutoScrollOnExpand, toAutoTristateTracking, toAutoDeleteMovedNodes];
  VstStationList.TreeOptions.MiscOptions := [toCheckSupport, toFullRepaintOnResize, toGridExtensions, toInitOnSave, toWheelPanning];
  VstStationList.TreeOptions.PaintOptions := [toHideFocusRect, toThemeAware, toUseBlendedImages];
  VstStationList.TreeOptions.SelectionOptions := [toDisableDrawSelection, toExtendedFocus, toFullRowSelect, toMiddleClickSelect, toRightClickSelect];
  VstStationList.TreeOptions.StringOptions := [toSaveCaptions];

  // Sort direction
  VstStationList.Header.SortDirection := TSortDirection.sdAscending;
  VstStationList.Header.SortColumn := 0;

  // Scroll bars
  VstStationList.ScrollBarOptions.ScrollBars := ssVertical;

end;

Dodajemy trzy kolumny oraz ustawiamy wysokość wierszy, domyślne sortowanie i widoczność pasków przewijania. Podpinamy również zdarzenia opisane poniżej. Do konfiguracji nagłówków dodajemy możliwość zmiany szerokości hoColumnResize oraz ikonę kierunku sortowania hoShowSortGlyphs. Z opcji zaznaczania SelectionOptions usuwamy toCenterScrollIntoView, który powoduje że cały czas aktywny jest środkowy z wyświetlonych wierszy.

Zdarzenie OnBeforeItemErase

Wołane jest tuż przed wyczyszczeniem wiersza. Używamy do kolorowania co drugiego wiersza oraz do oznaczenia aktualnie odtwarzanej stacji

procedure TMainForm.VstStationListBeforeItemErase(Sender: TBaseVirtualTree;
  TargetCanvas: TCanvas; Node: PVirtualNode; const ItemRect: TRect;
  var ItemColor: TColor; var EraseAction: TItemEraseAction);
var
  Data: PStationNodeRec;
begin
  if (not Sender.Selected[Node]) then
  begin
    Data := Sender.GetNodeData(Node);

    // Colorize line with currently playing station
    if (Data^.snd.ID = RadioPlayer.CurrentStationId) then
    begin
      ItemColor := clRed;
      EraseAction := eaColor;
    end
    else
    // Coloring every second line
    if (Odd(Node^.Index)) then
    begin
      ItemColor := clGray;
      EraseAction := eaColor;
    end;
  end;
end;

PStationNodeRec jest wskaźnikiem na rekord reprezentujący dane danego wiersza, czyli:

type
  TStationNodeData = class
  protected
    FID      : string;
    FName    : string;
    FGenre   : string;
    FCountry : string;
  public
    constructor Create(const Id: string;
      const Name, Genre, Country: string); overload;

    property ID       : string   read FID       write FID;
    property Name     : string   read FName     write FName;
    property Genre    : string   read FGenre    write FGenre;
    property Country  : string   read FCountry  write FCountry;
  end;

  PStationNodeRec = ^TStationNodeRec;
  TStationNodeRec =
  record
     snd : TStationNodeData;
  end;
Zdarzenie OnHeaderClick

Wyzwalane jest po kliknięciu w nagłówek kolumny. Używamy do sortowania danych.

procedure TMainForm.VstStationListHeaderClick(Sender: TVTHeader;
  HitInfo: TVTHeaderHitInfo);
begin
  // We determine the sort direction but only if click on the same column
  if (Sender.SortColumn = HitInfo.Column) then
  begin
    if Sender.SortDirection = sdAscending then
      Sender.SortDirection := sdDescending
    else
      Sender.SortDirection := sdAscending;
  end;

  // show SortGlyph
  Sender.SortColumn := HitInfo.Column;

  // sorting
  Sender.Treeview.SortTree(HitInfo.Column, Sender.SortDirection, True);
end;
Zdarzenie OnFreeNode

Wyzwalane jest tuż przed zwolnieniem danego węzła (noda).

procedure TMainForm.VstStationListFreeNode(Sender: TBaseVirtualTree;
  Node: PVirtualNode);
var
  Data: PStationNodeRec;
begin
  Data := Sender.GetNodeData(Node);

  if not Assigned(Data) then
    Exit;

  if Data^.snd <> nil then
    Data^.snd.Free;

  Finalize(Data^);
end;
Zdarzenie OnCompareNodes

Wyzwalane jest przy sortowaniu i przeszukiwaniu węzłów (nodów). Zmienna Column wskazuje na kolumnę, według której porównujemy węzły.

procedure TMainForm.VstStationListCompareNodes(Sender: TBaseVirtualTree; Node1,
  Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer);
var
  Data1: PStationNodeRec;
  Data2: PStationNodeRec;
begin
  Data1 := Sender.GetNodeData(Node1);
  Data2 := Sender.GetNodeData(Node2);

  if (not Assigned(Data1) or (Data1^.snd = nil)) or
     (not Assigned(Data2) or (Data2^.snd = nil)) then
    Result := 0
  else begin
    case Column of
      0: Result := CompareText(Data1^.snd.Name, Data2^.snd.Name);
      1: Result := CompareText(Data1^.snd.Genre, Data2^.snd.Genre);
      2: Result := CompareText(Data1^.snd.Country, Data2^.snd.Country);
    end;
  end;
end;
Zdarzenie OnGetText

Wyzwalane jest każdorazowo przy pobieraniu tekstu dla danego węzła i kolumny. Czyli właśnie w tym zdarzeniu wskazujemy co ma zostać wyświetlone w kolumnach.

procedure TMainForm.VstStationListGetText(Sender: TBaseVirtualTree;
  Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
  var CellText: String);
var
  Data: PStationNodeRec;
begin
  Node^.States := Node^.States +[vsOnFreeNodeCallRequired];
  // A handler for the OnGetText event is always needed as it provides
  // the tree with the string data to display.
  // Note that we are always using WideString
  Data := Sender.GetNodeData(Node);

  if Assigned(Data) and (Data^.snd <> nil) then
  begin
    case Column of
      0: CellText := Data^.snd.Name;
      1: CellText := Data^.snd.Genre;
      2: CellText := Data^.snd.Country;
    end;
  end;
end;  
Zdarzenie OnDblClick

Wyzwalane po podwójnym kliknięciu w wiersz.

procedure TMainForm.VstStationListDblClick(Sender: TObject);
var
  node: PVirtualNode;
  data: PStationNodeRec;
begin
  node := VstStationList.GetFirstSelected;

  if node <> nil then
  begin
    data := VstStationList.GetNodeData(node);

    RadioPlayer.PlayStation(data^.snd.ID);
  end;
end;
Zdarzenie OnKeyPress

Wyzwalane po naciśnięciu klawisza.

procedure TMainForm.VstStationListKeyPress(Sender: TObject; var Key: char);
begin
  if (Key = Char(VK_RETURN)) then
  begin
    // set #0 to eliminate Ding sound
    Key := #0;
  end
end; 
Zdarzenie OnFocusChanged

Wyzwalane, gdy fokus węzła ma się zmienić. Może się przydać np. do zmiany wysokości aktualnie zaznaczonego węzła.

procedure TMainForm.VstStationListFocusChanged(Sender: TBaseVirtualTree;
  Node: PVirtualNode; Column: TColumnIndex);
begin
  with TVirtualStringTree(Sender) do
  begin
    if OldNode <> nil then
      NodeHeight[OldNode] := DefaultNodeHeight;

    if Node <> nil then
    begin
      NodeHeight[Node] := round(DefaultNodeHeight * 1.5);
      OldNode := Node;
    end;
  end;
end;
Zdarzenie OnGetNodeDataSize

Wyzwalane, gdy dostęp do danych węzła następuje po raz pierwszy, ale rozmiar danych nie jest jeszcze ustawiony.

procedure TMainForm.VstStationListGetNodeDataSize(Sender: TBaseVirtualTree;
  var NodeDataSize: Integer);
begin
  NodeDataSize := SizeOf(TStationNodeRec);
end;  
Zdarzenie OnAfterColumnWidthTracking

Wyzwalane przy zmianie szerokości kolumny. Możemy użyć do zapisu ustawień.

procedure TMainForm.VstStationListAfterColumnWidthTracking(Sender: TVTHeader;
  Column: TColumnIndex);
begin
  case Column of
    0: TTRPSettings.SetValue('StationList.ColumnWidth.StationName', Sender.Columns[Column].Width);
    1: TTRPSettings.SetValue('StationList.ColumnWidth.Genre', Sender.Columns[Column].Width);
    2: TTRPSettings.SetValue('StationList.ColumnWidth.Country', Sender.Columns[Column].Width);
  end;
end;  
Zdarzenie OnHeaderDrawQueryElements

Wskazujemy, że tło nagłówków kolumn będziemy rysować osobiście. Powiązane z kolejnym zdarzeniem OnAdvancedHeaderDraw.

procedure TMainForm.VstStationListHeaderDrawQueryElements(Sender: TVTHeader;
  var PaintInfo: THeaderPaintInfo; var Elements: THeaderPaintElements);
begin
    Elements := [hpeBackground];
end; 
Zdarzenie OnAdvancedHeaderDraw

Rysujemy tło nagłówków kolumn

procedure TMainForm.VstStationListAdvancedHeaderDraw(Sender: TVTHeader;
  var PaintInfo: THeaderPaintInfo; const Elements: THeaderPaintElements);
begin
  if hpeBackground in Elements then
  begin
    PaintInfo.TargetCanvas.Brush.Color := TSkins.GetColorItem('StationList.Header.BackgroundColor');
    PaintInfo.TargetCanvas.FillRect(PaintInfo.PaintRectangle);
  end;
end;

Konfigurację mamy gotową więc w kolejnym wpisie przejdziemy do dodawania, edycji i usuwania danych.

Kod aplikacji dostępny jest na GitHubie.