W poprzednim wpisie pisałem o tym, że zamiast wyznaczać ścieżki do obrazków, lepiej umieścić je w zasobach. Temat wydawał mi się prosty, ale w komentarzach pojawiła się odrobinę odmienna opinia:
"Nie, obrazków nie trzyma się w zasobach. Ikony, czasem, jak potrzeba, jeden, dwa obrazki, ale nie więcej. One są za duże - EXE-k puchnie, robi się bałagan, trzeba rekompilować, by zmienić obrazek, a i krótsze nie jest. Tą linijkę równie dobrze można zapisać jako:
ImageBrush brush = new ImageBrush(new BitmapImage(new Uri("Images/test.png", UriKind.Relative)));
Można to wpakować w jakąś statyczną klasę helpera, by nie pisać nie wiadomo ile razy."
W tym wpisie postaram się wyjaśnić i uzupełnić informacje związane z zasobami w WPF. Moim celem nie jest kompletna negacja przedstawionej opinii, ale przyjrzyjmy się, co daje nam .NET Framework i jak to można wykorzystać, by nasza aplikacja była lepsza.
Zacznijmy od prostego stwierdzenia: są dwa typy zasobów:
-
Zasoby w sensie XAML
-
Zasoby w sensie plików osadzonych w jakimś assembly
Zasoby (resources) w XAML
Zauważmy, że WPF pozwala nam definiować zasoby w XAML, można to zrobić bezpośrednio w kodzie XAML okna lub kontrolki (w poprzednim wpisie właśnie był taki przykład) lub lepiej zasób zdefiniować w pliku App.xaml (wspólnym dla całej aplikacji) lub najlepiej w ResourceDictionary.
Zobaczmy poniższy przykład, w którym zasób zadeklarowano w pliku App.xaml:
<Application x:Class="WpfApplication_resources_tests.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
<BitmapImage x:Key="MyAppImageSource" UriSource="./ImagesToBeCopied/logo_zielone_tlo.jpg" />
<ImageBrush x:Key="MyAppImageBrush" ImageSource="{StaticResource ResourceKey=MyAppImageSource}"/>
</Application.Resources>
</Application>
Zauważmy, że obrazek nie został dodany do assembly, a tylko skopiowany do katalogu aplikacji:
I wykorzystano go poprzez:
<Window x:Class="WpfApplication_resources_tests.Window2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window2" Height="300" Width="300">
<Grid>
<Label Background="{StaticResource ResourceKey=MyAppImageBrush}" Width="50" Height="50"
VerticalAlignment="Top" HorizontalAlignment="Left"/>
<Viewport3D Name="mainViewport">
<Viewport3D.Camera>
<PerspectiveCamera FarPlaneDistance="100" LookDirection="-1,-1,-1"
UpDirection="0,1,0" NearPlaneDistance="1" Position="3,3,3" FieldOfView="70" />
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<DirectionalLight Color="White" Direction="-1,-1,-1" />
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="-2 -2 0 2 -2 0 -2 2 0 2 2 0"
Normals="0 0 1 0 0 1 0 0 1 0 0 1" TextureCoordinates="0 1 1 1 0 0 1 0 "
TriangleIndices="0 1 2 1 3 2" />
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial Brush="{StaticResource ResourceKey=MyAppImageBrush}" />
</GeometryModel3D.Material>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
</Grid>
</Window>
W wyniku otrzymamy okno dokładnie takie jak poprzednio. Oczywiście można to zrobić przez ResourceDictionary (i jest to sposób zalecany przez Ms). Zaletą takiego podejścia są:
-
Mamy jedno miejsce, w którym nastąpiła deklaracja zasobu.
-
Łatwo możemy się do zasobu odwołać.
-
Jest to standardowy sposób i każdy, kto programuje w WPF, będzie wiedział jak z tego skorzystać.
-
Nie tworzymy żadnych „helperów”, bo nie ma takiej potrzeby. Nie wynajdujemy koła na nowo!!
Zasoby (resources) w assembly
Dodatkowe pliki dla aplikacji można trzymać bezpośrednio w assembly. Pisałem o tym już wcześniej w artykule: "Prosta obsługa Resource’ów aplikacji .NET (C#)". Teraz skupmy się jednak na wykorzystaniu tego w WPF. Chciałbym tutaj podkreślić, że nie jestem fanem trzymania informacji w plikach EXE i nie zalecam tam tego robić! Nie jest jednak dobrym podejściem trzymanie grafik, w taki sposób, że każda grafika (lub dowolny inny zasób) znajduje się w osobnym pliku. Przecież nie chcemy narażać się na potencjalne problemy z deployment'em – trzeba pamiętać, co było dodawane. Zastanówmy się również, czy chcemy pozwolić każdemu (nawet niedoświadczonemu) użytkownikowi na zmienianie naszej aplikacji (prawie każdy potrafi podmienić obrazek w jakimś katalogu, czy tego na prawdę chcemy?). Do zgromadzenia naszych zasobów lepiej wykorzystać odrębne assembly. A następnie można w XAML zdefiniować zasoby przy pomocy adresacji PACK.
<BitmapImage x:Key="MyImageSource"
UriSource=
"pack://application:,,,/MyResourcesLibrary;component/puzzle.png" />
Wspomniane assembly nie musi być nawet dodane do projektu jako referencja (można je podmieniać później bez rekompilacji). To jak łatwo to można zrobić pokazuje poniższy rysunek:
Literatura
Polecam również przeczytać więcej na ten temat:
-
MSDN: WPF Application Resource, Content, and Data Files: http://msdn.microsoft.com/en-us/library/aa970494.aspx
-
MSDN: Pack URIs in WPF: http://msdn.microsoft.com/en-us/library/aa970069.aspx
-
Open Packaging Conventions (OPC, tylko trochę inne niż do tej pory wspominane na tym blogu): http://msdn.microsoft.com/en-us/windows/hardware/gg463375.aspx
Pamiętajcie, że lepiej wykorzystać coś, co ktoś już dla nas przygotował, są zasoby – korzystajmy z nich, nie wynajdujmy koła na nowo!
PS
Mam nadzieję, że nie uraziłem nikogo. Jeżeli tak, to przepraszam, ale nie taki był mój zamiar.












Maciej_Zbrzezny on delicious.com
Wielkie dzięki.. miałem trochę kłopotu z ikonkami w programie, gdy uruchamiałem go z instalki. Trafiłem na ten wpis i wszystko stalo sie proste. Wielkie dzieki jeszcze raz :)
OdpowiedzUsuńCo zrobić, kiedy VS2013 nie widzi mojego assembly z zasobami, tzn wyświetlanie ostrzeżenie (po najechaniu na linię z odwołaniem 'UriSource="pack://application:,,,' w xaml)
OdpowiedzUsuń"Could not load file or assembly 'MojeZasoby, Culture=Neutral' or one of its dependencies."?
Zrobiłem wszystko jak na instrukcji z obrazka oprócz "Copied in Build event (not referenced)", bo nie mam pojęcia jak to zrobić.