Systém Windows XP již před nějakou dobou přinesl zobrazení standardních ovládacích prvků pomoci stylů - Visual Styles a možnost přepínání způsobu zobrazení pomoci grafických témat – Desktop themes. Ve Windows Vista bylo zavedeno zobrazení Aero Glass a s tím vznikl i nový grafický styl kontrolů, který zůstal i v následujících Windows 7. Nyní Microsoft ve Windows 8 opouští Aero styl a přináší na desktop design, který se více blíží tabletovému rozhraní Metro. Jsou zde hned dvě nová zobrazení: Aero 2 výchozí pro Windows 8 a AeroLite výchozí ve Windows Server 2012 (*).
Pokud připočítáme i klasické zobrazení, které se používá v případě vypnutých visual styles, máme tím celkem již 8 různých stylů pro zobrazení kontrolů. Tady je jejich přehled a ukázka:
Theme |
Native |
WPF |
System |
Označení |
Classic |
|
|
Zobrazení bez visual styles |
classic |
Luna |
|
|
Windows XP (Default blue theme) |
luna.normalcolor |
Luna Olive Green |
|
|
Windows XP (Olive theme) |
luna.homestead |
Luna Silver |
|
|
Windows XP (Silver theme) |
luna.metallic |
Royale |
|
|
Windows XP Media Center Edition (lze doinstalovat do Windows XP) |
royale.normalcolor |
Aero |
|
|
Windows 7, Windows Vista |
aero.normalcolor |
Aero2 |
|
|
Windows 8, Windows Server 2012 (*) |
aero2.normalcolor |
AeroLite |
|
|
Windows Server 2012, Windows 8 (*) |
aerolite.normalcolor |
WPF
Na rozdíl od visual styles, které jsou obsaženy v systému (natvrdo pro daný systém), ve WPF je vzhled kontrolů vytvářen pomoci XAML stylů. Výchozí styly pak kopírují vzhled standardních visual styles témat. Pro zabudované kontroly jsou tyto styly obsaženy v samostatných assembly jako PresentationFramework.Classic.dll, PresentationFramework.Luna.dll, PresentationFramework.Royale.dll, PresentationFramework.Aero.dll. Nové zobrazení Windows 8 Aero 2 a AeroLite je zajištěno tím, že .NET Framework 4.5 (který je ve Windows 8 rovnou předinstalován) přidává ještě assembly PresentationFramework.Aero2.dll a PresentationFramework.AeroLite.dll s patřičnými styly (inplace k .NET 4.0). Proto pokud spustíte na Windows 8 aplikaci ve WPF ve FW 3.5, nebudou kontroly zobrazeny ve správných stylech (použije se Windows 7 Aero místo Aero2 a Classic styl místo AeroLite).
Styly Aero2 a AeroLite se navzájem až na pár drobných změn ve Scrollbaru, ComboxBoxu a ListView neliší. Je patrné z obrázků, že tlačítka v Aero2 vypadají stejně jako AeroLite (tedy rozdílně než nativní).
Styly pro vlastní Custom Controls
Pokud vytváříme vlastní kontrol, kterému potřebujeme vytvořit i vlastní nový styl (např. pokud nelze využít styl z base kontrolu) a chceme, aby kontrol vypadal stejně jako základní kontroly, měli bychom tento styl vytvořit pro všechny Windows témata (nebo pro ty, pro které je aplikace podporována).
Ukážeme si jak nastavit použití jednotlivých stylů. Mějme například kontrol SplitButton, jeho styl nemůžeme pouze odvodit z Button stylu, ale chceme, aby vypadal podobně na všech systémech jako klasický Button. Musíme splnit následující:
1) Kontrol má definováno použití stylu tímto statickým konstruktorem:
static SplitButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(SplitButton), new FrameworkPropertyMetadata(typeof(SplitButton)));
}
2) Dále musíme vytvořit příslušné ResourceDictionary se styly kontrolu. XAML soubor s touto ResourceDictionary musí mít konkrétní jméno a musí být umístěn v podsložce projektu Themes. Jsou možné tyto jména souborů:
Classic.xaml – klasické zobrazení s vypnutými visual styles
Luna.NormalColor.xaml - Windows XP výchozí blue theme
Luna.Homestead.xaml - Windows XP Olive theme
Luna.Metallic.xaml - Windows XP Silver theme
Royale.NormalColor.xaml - Windows XP MCE Royale theme
Aero.NormalColor.xaml - Windows Vista / Windows 7 Aero theme
Aero2.NormalColor.xaml - Windows 8 theme
AeroLite.NormalColor.xaml - Windows Server 2012 výchozí theme
Generic.xaml - generic non-theme specific resource
Soubor ResourceDictionary může styl obsahovat buď přímo, nebo je vhodné pouze uvést odkaz na ResourceDictionary se stylem v jiném souboru.
Příklad ResourceDictionary souboru Aero2.NormalColor.xaml, který obsahuje odkaz na samostatný XAML soubor se stylem pro Aero2 theme může vypadat takto:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/SharedControls;component/Styles/SplitButton_Aero2.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
3) Na úrovni naší assembly musíme definovat, že obsahuje ResourceDictionary soubory specifické pro jednotlivá témata. To provedeme nastavením atributu ThemeInfo v AssemblyInfo.cs na SourceAssembly (výchozí nastavení je, že obsahuje pouze generic soubor).
// Specifies the location in which theme dictionaries are stored for types in an assembly.
[assembly: ThemeInfo(
// Specifies the location of system theme-specific resource dictionaries for this project.
ResourceDictionaryLocation.SourceAssembly,
// Specifies the location of the system non-theme specific resource dictionary.
ResourceDictionaryLocation.SourceAssembly
)]
Jaký theme soubor je vybrán?
Aplikace nemusí obsahovat výše uvedené ResourceDictionary soubory pro všechny témata. Z jakého souboru se tedy nahrají resource v případě, že některé themes budou chybět? Při výběru se postupuje takto:
- Podle nastavení ve Windows je načteno požadované jméno theme (a barevné schéma) (**).
- Pokud resource pro požadovaný theme není nalezen, použije se Classic resource.
- Pokud neexistuje ani Classic resource, je použit Generic resource.
Je dobré s tímto počítat. V praxi totiž většinou styl pro Classic budeme mít (pro zobrazení bez visual styles), a ten se nám zobrazí i v případě nepodporovaného themu (což ale většinou nechceme). (Podle mě by bylo lepší, kdyby se v případě, že resource neexistuje, nehledal Classic, ale rovnou by se použil Generic).
Proto je dobré přidat soubory resource dictionary pro všechna témata, s tím, že pokud pro ně už nevytvoříme samostatný styl, tak aspoň nastavíme použití některého jiného stylu než Classic (například aby AeroLite použil Aero2, nebo aby oba Aero2 a AeroLite použily Aero, atd.).
Získání výchozích stylů
Pokud tvoříme styly pro náš kontrol, bylo by dobré vyjít z existujících výchozích stylů základních kontrolů a ty upravovat (například pro náš SplitButton kontrol upravíme styly Button kontrolu). Zde se k tomu moc nehodí použití Expression Blendu (které jsme si popsali zde pro Silverlight), protože nelze určit, pro který theme chceme styl vytvářet. Blend totiž vytvoří styl podle aktuálního nastavení theme ve Windows. (Pokud bychom tedy chtěli převzít styl např. pro Windows XP Luna, museli bychom to provádět na Windows XP.)
Získat styl přímo z resoure assembly (např. z PresentationFramework.Aero2.dll ) také není možné, protože XAML resource dictionary jsou pro WPF v assembly zkompilované v BAML (Binary Application Markup Language), např. aero2.normalcolor.baml. Pokud je mi známo, tak pro .NET 4.0 (zatím) neexistuje žádný fungující baml viewer / decompilation nástroj.
Jak tedy získat výchozí styl pro konkrétní theme? Využijeme k tomu blend, ale trochu jinak. Využijeme styly, které blend používá v případě volby Edit Template / Edit a Copy. Ty jsou po instalaci blendu (pro Blend Preview for Visual Studio 2012) v adresáři %ProgramFiles(x86)%\Microsoft Visual Studio 11.0\Blend Preview\SystemThemes\Wpf, zde najdete pro jednotlivé themes patřičné xaml soubory se styly všech základních kontrolů (např. aero2.normalcolor.xaml). Z nich už pak jen stačí ručně vykopírovat ten, který zrovna potřebujete.
Tip pro zobrazení stylu
Závěrem ještě jeden tip pro usnadnění ladění vzhledu stylu konkrétního themu. Abychom nemuseli přepínat na konkrétní theme nebo spouštět aplikaci (nedej bože provádět vývoj) pod konkrétním starším systémem, můžeme si v XAML přímo nastavit, který styl se má aktuálně použít.
Například tento XAML kód zobrazí Button kontrol ve všech 8 stylech:
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Content="Aero2" Width="105" Height="50">
<Button.Resources>
<ResourceDictionary Source="/PresentationFramework.Aero2, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35;component/themes/aero2.normalcolor.xaml" />
</Button.Resources>
</Button>
<Button Grid.Column="1" Content="AeroLite" Width="105" Height="50">
<Button.Resources>
<ResourceDictionary Source="/PresentationFramework.Aerolite, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35;component/themes/aerolite.normalcolor.xaml" />
</Button.Resources>
</Button>
<Button Grid.Column="2" Content="Aero" Width="105" Height="50">
<Button.Resources>
<ResourceDictionary Source="/presentationframework.Aero, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35;component/themes/aero.normalcolor.xaml" />
</Button.Resources>
</Button>
<Button Grid.Column="3" Content="Classic" Width="105" Height="50">
<Button.Resources>
<ResourceDictionary Source="/presentationframework.Classic, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35;component/themes/classic.xaml" />
</Button.Resources>
</Button>
<Button Grid.Column="0" Grid.Row="1" Content="Luna" Width="105" Height="50">
<Button.Resources>
<ResourceDictionary Source="/presentationframework.Luna, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35;component/themes/luna.normalcolor.xaml" />
</Button.Resources>
</Button>
<Button Grid.Column="1" Grid.Row="1" Content="Luna
Olive Green" Width="105" Height="50">
<Button.Resources>
<ResourceDictionary Source="/presentationframework.Luna, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35;component/themes/luna.homestead.xaml" />
</Button.Resources>
</Button>
<Button Grid.Column="2" Grid.Row="1" Content="Luna
Silver" Width="105" Height="50">
<Button.Resources>
<ResourceDictionary Source="/presentationframework.Luna, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35;component/themes/luna.metallic.xaml" />
</Button.Resources>
</Button>
<Button Grid.Column="3" Grid.Row="1" Content="Royale" Width="105" Height="50">
<Button.Resources>
<ResourceDictionary Source="/presentationframework.Royale, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35;component/themes/royale.normalcolor.xaml" />
</Button.Resources>
</Button>
</Grid>
Podobně pro náš vlastní kontrol:
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<local:SplitButton Grid.Column="0" Content="Aero2" Width="105" Height="50">
<local:SplitButton.Resources>
<ResourceDictionary Source="/SharedControls;component/themes/aero2.normalcolor.xaml" />
</local:SplitButton.Resources>
</local:SplitButton>
<local:SplitButton Grid.Column="1" Content="AeroLite" Width="105" Height="50">
<local:SplitButton.Resources>
<ResourceDictionary Source="/SharedControls;component/themes/aerolite.normalcolor.xaml" />
</local:SplitButton.Resources>
</local:SplitButton>
<local:SplitButton Grid.Column="2" Content="Aero" Width="105" Height="50">
<local:SplitButton.Resources>
<ResourceDictionary Source="/SharedControls;component/themes/aero.normalcolor.xaml" />
</local:SplitButton.Resources>
</local:SplitButton>
<local:SplitButton Grid.Column="3" Content="Classic" Width="105" Height="50">
<local:SplitButton.Resources>
<ResourceDictionary Source="/SharedControls;component/themes/classic.xaml" />
</local:SplitButton.Resources>
</local:SplitButton>
<local:SplitButton Grid.Column="0" Grid.Row="1" Content="Luna" Width="105" Height="50">
<local:SplitButton.Resources>
<ResourceDictionary Source="/SharedControls;component/themes/luna.normalcolor.xaml" />
</local:SplitButton.Resources>
</local:SplitButton>
<local:SplitButton Grid.Column="1" Grid.Row="1" Content="Luna
Olive Green" Width="105" Height="50">
<local:SplitButton.Resources>
<ResourceDictionary Source="/SharedControls;component/themes/luna.homestead.xaml" />
</local:SplitButton.Resources>
</local:SplitButton>
<local:SplitButton Grid.Column="2" Grid.Row="1" Content="Luna
Silver" Width="105" Height="50">
<local:SplitButton.Resources>
<ResourceDictionary Source="/SharedControls;component/themes/luna.metallic.xaml" />
</local:SplitButton.Resources>
</local:SplitButton>
<local:SplitButton Grid.Column="3" Grid.Row="1" Content="Royale" Width="105" Height="50">
<local:SplitButton.Resources>
<ResourceDictionary Source="/SharedControls;component/themes/royale.normalcolor.xaml" />
</local:SplitButton.Resources>
</local:SplitButton>
</Grid>
Uvedený příklad kontrolu SplitButton se styly všech témat je ke stažení zde.
(*) V obou systémech jsou dostupná jak Aero2 tak AeroLine visual style. Ve Windows Server 2012 je výchozí AeroLite styl (nastavení Windows Basic), pokud ale doinstalujeme Desktop Experience Feature, můžeme přepnout i na Aero2 (Windows theme). Windows 8 obsahuje na výběr pouze témata s Aero2, a pokud je mi známo nelze z control panelu AeroLite nastavit. Pokud ale ručně zeditujete příslušný soubor .theme (v cestě c:\Users\<user>\AppData\Local\Microsoft\Windows\Themes), můžete zde místo Aero.msstyles nastavit i použití AeroLite.msstyles.
(**) Pokud nejsou zapnuté themes (nebo je vybrán high contrast), vybere se Classic. Jinak je načten pomoci API funkce GetCurrentThemeName používaný theme a barevné schéma ve Windows. Pokud se jedná o Aero a systém Windows 8, je místo něj hledán theme Aero2 (protože funkce GetCurrentThemeName na Windows 8 vrací místo Aero2 také Aero jako na Windows Vista/7).
Update: Jak jsem popsal výše, aplikace pro FW 3.5 nemají nové Aero2 styly. Zde jsou k dispozici moje upravené Aero2 styly pro FW 3.5 (pro ty controly, které jsou ve FW 3.5).
Jejich načtení pak můžeme zařídit např. touto funkcí LoadAero2StylesResourceDictionary:
[SuppressUnmanagedCodeSecurity, SecurityCritical, DllImport("uxtheme.dll", CharSet = CharSet.Auto)]
private static extern int GetCurrentThemeName(StringBuilder pszThemeFileName, int dwMaxNameChars, StringBuilder pszColorBuff, int dwMaxColorChars, StringBuilder pszSizeBuff, int cchMaxSizeChars);
public static void LoadAero2StylesResourceDictionary(ResourceDictionary resources)
{
string themeName = GetThemeName();
if (string.Equals(themeName, "Aero2", StringComparison.OrdinalIgnoreCase) || string.Equals(themeName, "AeroLite", StringComparison.OrdinalIgnoreCase))
{
var aero2StylesResources = new ResourceDictionary() { Source = GetResourceUri("Styles/Aero2Styles.xaml") };
resources.MergedDictionaries.Add(aero2StylesResources);
}
}
private static string GetThemeName()
{
string themeName = string.Empty;
StringBuilder pszThemeFileName = new StringBuilder(260);
StringBuilder pszColorBuff = new StringBuilder(260);
if (GetCurrentThemeName(pszThemeFileName, pszThemeFileName.Capacity, pszColorBuff, pszColorBuff.Capacity, null, 0) == 0)
{
themeName = pszThemeFileName.ToString();
themeName = System.IO.Path.GetFileNameWithoutExtension(themeName);
if ((string.Compare(themeName, "aero", StringComparison.OrdinalIgnoreCase) == 0) && IsOSWindows8OrNewer)
{
themeName = "Aero2";
}
}
return themeName;
}
private static bool IsOSWindows8OrNewer
{
get { return (Environment.OSVersion.Version >= new Version(6, 2)); }
}
private static Uri GetResourceUri(string relativeFile)
{
string assemblyShortName = System.Reflection.Assembly.GetExecutingAssembly().FullName.Split(',')[0];
return new Uri("/" + assemblyShortName + ";component/" + relativeFile, UriKind.Relative);
}
Některé další zdroje:
http://www.codeproject.com/Articles/35444/Defining-the-Default-Style-for-a-Lookless-Control
http://msdn.microsoft.com/en-us/library/ms745025.aspx
http://blogs.msdn.com/b/wpfsdk/archive/2007/07/31/using-themes-with-custom-controls.aspx
http://arbel.net/2006/11/03/forcing-wpf-to-use-a-specific-windows-theme
http://social.msdn.microsoft.com/Forums/da-DK/wpf/thread/e3dd4221-af4d-4ae4-a983-895db12ebcd0