Tym razem chciałbym opisać mechanizm zmiany skórek w naszej aplikacji.
Założenia
- Każda skórka zostanie zapisana w osobnym pliku archiwum zip
- W jego skład wejdą pliki graficzne png oraz plik xml z dodatkową konfiguracją
- Wszystkie nazwy plików ze skórkami zostaną wczytane podczas startu aplikacji.
- Zmiana skórki będzie możliwa z poziomu menu głównego.
Tworzenie pliku ze skórką
Spakowany plik nazwa_skorki.zip zawierał będzie pliki graficzne *.png oraz plik skin.xml. Struktura plików może wyglądać mniej więcej tak:
Pliki graficzne prezentują po prostu elementy, które chcemy podmienić np. przyciski. W pliku skin.xml możemy natomiast opisać całą resztę konfiguracji np. wszystkie elementy, którym chcemy zmienić kolory, rozmiar, położenie:
Wczytanie danych o skórkach
Aby wczytać listę skórek, musimy przeszukać katalog, w którym się one znajdują. Zapisujemy tylko nazwy plików bez pełnej ścieżki i rozszerzeń.
const SKINS_PATH = 'data/skins/'; SKIN_FILE_EXTENSION = '.zip'; SKIN_FILE_PATTERN = '*' + SKIN_FILE_EXTENSION; class procedure TSkins.SearchForAllSkinsFiles(); var files: TStringList; i: Integer; begin if not Assigned(SkinFiles) then SkinFiles := TStringList.Create else SkinFiles.Clear; files := FindAllFiles(ConcatPaths([GetApplicationPath, SKINS_PATH]), SKIN_FILE_PATTERN, false); try for i := 0 to Pred(files.Count) do SkinFiles.Add(RemoveFileExtension(ExtractFileName(files[i]))); finally files.Free; end; end;
Wczytanie domyślnej skórki
Z ustawień aplikacji pobieramy nazwę domyślnej skórki.
class function TSkins.GetSkinFileName(const UseDefaultSkin: Boolean): string; var skinName: string; begin // try to get the skin name from the settings if UseDefaultSkin then skinName := DEFAULT_SKIN else skinName := TTRPSettings.GetValue('SelectedSkin', EMPTY_STR); if skinName = EMPTY_STR then skinName := DEFAULT_SKIN; TTRPSettings.SetValue('SelectedSkin', skinName); Result := skinName; end;
Następnie próbujemy wczytać plik i wypakować jego składowe. Dodatkowo trzeba pamiętać, aby podać dokładne nazwy zasobów, które mają zostać wypakowane.
class function TSkins.GetSkinResources(const SkinName: string): Boolean; var zipFile: TUnZipper; items: TStringList; begin Result := false; CurrentSkinName := SkinName; items := TStringList.Create; try // Search icon items.Add('icoSearch.png'); // Bottom function panel items.Add('btnPlay.png'); items.Add('btnPause.png'); items.Add('btnPrev.png'); items.Add('btnStop.png'); items.Add('btnNext.png'); items.Add('btnRec.png'); items.Add('btnOpen.png'); // Station List Popup Menu items.Add('btnAdd.png'); items.Add('btnEdit.png'); items.Add('btnDelete.png'); // xml items.Add('skin.xml'); zipFile := TUnZipper.Create; try zipFile.FileName := GetSkinFilePath(SkinName); zipFile.OnOpenInputStream := @zipFileOpenInputStream; zipFile.OnCreateStream := @zipFileCreateStream; zipFile.OnDoneStream := @zipFileDoneStream; zipFile.OnCloseInputStream := @zipFileCloseInputStream; zipFile.UnZipFiles(items); finally FreeAndNil(zipFile); end; finally FreeAndNil(items); end; Result := true; end;
Klasa TUnZipper działa w taki sposób, że po każdej akcji np. wypakowaniu pliku, emitowane jest zdarzenie pod które możemy się podpiąć. W naszym przypadku zdarzenie OnDoneStream umożliwi zapisanie w pamięci plików graficznych oraz deserializację konfiguracji.
class procedure TSkins.zipFileDoneStream(Sender: TObject; var AStream: TStream; AItem: TFullZipFileEntry); var xmlNode: TDOMNode; xmlDoc: TXMLDocument; i: integer; begin if AItem.DiskFileName = 'skin.xml' then begin AStream.Position := 0; // Load xml file from stream ReadXMLFile(xmlDoc, AStream); try xmlNode := xmlDoc.FirstChild; if Assigned(xmlNode) then begin // Skin items with xmlNode.ChildNodes do begin try for i := 0 to Count - 1 do begin if Item[i].NodeName = 'Item' then begin if (Item[i].Attributes.GetNamedItem('name') <> nil) and (Item[i].FirstChild <> nil) then begin FSkinData.AddItem( Item[i].Attributes.GetNamedItem('name').NodeValue, Item[i].FirstChild.NodeValue); end; end; end; finally Free; end; end; end; finally // Finally, free the xml document xmlDoc.Free; end; end else begin // Load bitmaps FSkinData.AddBitmap(RemoveFileExtension(AItem.DiskFileName), AStream); if Assigned(OnSkinDoneStream) then OnSkinDoneStream(Sender, AStream, AItem); end; Astream.Free; end;
Zdarzenie OnCloseInputStream emitowane jest po wczytaniu wszystkich plików i zamknięciu pliku ze skórką. Podepniemy tu nasze zdarzenie, które poinformuje o wczytaniu skórki.
procedure TSkins.zipFileCloseInputStream(Sender: TObject; var AStream: TStream); begin if Assigned(OnSkinLoaded) then OnSkinLoaded(Sender, FSkinData); end;
Teraz wystarczy już tylko podmienić odpowiednie elementy na GUI. Możemy to zrobić np. na głównej formatce aplikacji podpinając się pod zdarzenie OnSkinLoaded.
procedure TMainForm.LoadSkin; begin TSkins.OnSkinLoaded := @SkinLoaded; TSkins.LoadSkin(); end; procedure TMainForm.SkinLoaded(Sender: TObject; var ASkinData: TSkinData); begin // Panels SearchPanel.Background.Color := ASkinData.GetColorItem('SearchPanel.BackgroundColor'); StationListPanel.Background.Color := ASkinData.GetColorItem('StationListPanel.BackgroundColor'); MainPanel.Background.Color := ASkinData.GetColorItem('MainPanel.BackgroundColor'); // PeakmeterPanel PeakmeterPanel.Background.Color := ASkinData.GetColorItem('PeakmeterPanel.BackgroundColor'); PeakmeterPanel.Border.Color := ASkinData.GetColorItem('PeakmeterPanel.BorderColor'); // BottomFunctionPanel BottomFunctionPanel.Background.Gradient1.StartColor := ASkinData.GetColorItem('BottomFunctionPanel.Background.Gradient1.StartColor'); BottomFunctionPanel.Background.Gradient1.EndColor := ASkinData.GetColorItem('BottomFunctionPanel.Background.Gradient1.EndColor'); // SearchEdit SearchEdit.Color := TSkins.GetColorItem('SearchEdit.Color'); SearchEdit.Font.Color := TSkins.GetColorItem('SearchEdit.FontColor'); // Bitmaps miAddStation.Bitmap.Assign(ASkinData.GetBitmapItem('btnAdd')); miEditStation.Bitmap.Assign(ASkinData.GetBitmapItem('btnEdit')); miDeleteStation.Bitmap.Assign(ASkinData.GetBitmapItem('btnDelete')); btnPlay.Glyph.Assign(ASkinData.GetBitmapItem('btnPlay')); btnPrev.Glyph.Assign(ASkinData.GetBitmapItem('btnPrev')); btnStop.Glyph.Assign(ASkinData.GetBitmapItem('btnStop')); btnNext.Glyph.Assign(ASkinData.GetBitmapItem('btnNext')); btnOpen.Glyph.Assign(ASkinData.GetBitmapItem('btnOpen')); end;
Zmiana skórki
Listę stacji wczytujemy i umieszczamy np. w głównym menu aplikacji.
procedure TMainForm.AddMenuSkinItems; var i: integer; subItem: TMenuItem; begin for i := 0 to Pred(TSkins.SkinFiles.Count) do begin subItem := TMenuItem.Create(miSkins); subItem.Caption := TSkins.SkinFiles[i]; subItem.Tag := i; subItem.OnClick:= @miSkinsItemClick; // after clicking on the skin subItem.Checked := TSkins.CurrentSkinName = TSkins.SkinFiles[i]; miSkins.Add(subItem); end; end;
Pod każdą skórkę podpięliśmy zdarzenie OnClick, które po kliknięciu spowoduje załadowanie danej skórki.
procedure TMainForm.miSkinsItemClick(Sender: TObject); var i: integer; mi : TMenuItem; begin if Sender is TMenuItem then begin // mark the selected item and deselect all others mi := TMenuItem(Sender); for i := 0 to Pred(mi.Parent.Count) do mi.Parent.Items[i].Checked := mi.Parent.Items[i] = mi; TSkins.ChangeSkin(mi.Caption); end; end;
Końcowy efekt
Kod aplikacji dostępny jest na GitHubie. Bezpośredni link do pliku Skins.
- Tiny Radio Player #01 – Wprowadzenie
- Tiny Radio Player #02 – Instalacja komponentów
- Tiny Radio Player #03 – Roboczy interfejs aplikacji
- Tiny Radio Player #04 – Budujemy silnik
- Tiny Radio Player #05 – Zapis ustawień aplikacji
- Tiny Radio Player #06 – Zmiana języka aplikacji
- Tiny Radio Player #07 – Logowanie błędów
- Tiny Radio Player #08 – Baza danych
- Tiny Radio Player #09 – Zarządzanie bazą SQLite
- Tiny Radio Player #10 – Lista stacji radiowych, konfiguracja VirtualTreeView
- Tiny Radio Player #11 – Lista stacji radiowych, zarządzanie danymi w VirtualStringTree
- Jesteś tu => Tiny Radio Player #12 – Obsługa skórek (skins)