Skinned User Interface

来源:百度文库 编辑:神马文学网 时间:2024/07/02 19:43:52
This article discusses the basics of how to create a user interfacein WPF that can be "skinned" at runtime. We will examine the supportthat WPF provides for UI skinning, as well as review how a simple demoapplication puts those features to use.
The term "skin," when applied to user interfaces, refers to a visualstyle that is consistently applied to all elements in the UI. A"skinnable" UI can be re-styled either at compile-time or runtime. TheWindows Presentation Foundation provides great support for UI skinning.
There are various situations in which UI skinning might be importantfor an application. It can be used to allow the end-user to customizethe UI based on his or her personal aesthetic preferences. Anotherscenario in which skinning might be used is when a company creates oneapplication that is subsequently deployed to various clients. Eachclient might want their company logo, colors, font, etc. to bedisplayed by the application. If the UI is designed with skinning inmind, that task is easy to accomplish with minimal effort.
There are three fundamental pieces to this puzzle. This sectionprovides a brief overview of those topics. Refer to the "Externallinks" section at the end of the article for more information aboutthem. If you are already familiar with hierarchical resources, mergedresource dictionaries and dynamic resource references, feel free toskip this section.
To implement support for skinning, you must understand the basics ofhow the WPF resource system works. Many, many classes in WPF have apublic property named Resources, of type ResourceDictionary.This dictionary contains a list of key-value pairs where the key is anyobject and the value is a resource that can be any object, as well.Most often, the keys put into a ResourceDictionary are strings; sometimes they are Typeobjects. All resources are stored in these dictionaries and theresource lookup procedure uses them to find a requested resource.
The resource dictionaries in an application are arranged in ahierarchical fashion. When it comes time to locate a resource such as aBrush, Style, DataTemplate orany other type of object, the platform executes a lookup procedure thatnavigates up the resource hierarchy searching for a resource with acertain key.
It first checks the resources owned by the element requesting theresource. If the resource cannot be found there, it checks thatelement‘s parent to see if it has the requested resource. If the parentelement does not have the resource, it continues walking up the elementtree asking every ancestor element if it has a resource with therequested key. If the resource still cannot be found, it willeventually ask the Application object if it has the resource. For our purposes in this article, we can ignore what happens after that.
ResourceDictionary exposes a property that allows you to merge in resources from other ResourceDictionary instances, similar to a union in set theory. That property is named MergedDictionaries and is of type Collection.Here is an explanation from the SDK documentation which explains thescoping rules applied to resources in merged dictionaries:
Resources in a merged dictionary occupy a location in theresource lookup scope that is just after the scope of the main resourcedictionary they are merged into. Although a resource key must be uniquewithin any individual dictionary, a key can exist multiple times in aset of merged dictionaries. In this case, the resource that is returnedwill come from the last dictionary found sequentially in theMergedDictionaries collection. If the MergedDictionaries collection wasdefined in XAML, then the order of the merged dictionaries in thecollection is the order of the elements as provided in the markup. If akey is defined in the primary dictionary and also in a dictionary thatwas merged, then the resource that is returned will come from theprimary dictionary. These scoping rules apply equally for both staticresource references and dynamic resource references.
Refer to the "External links" section at the bottom of this articlefor a link to that help page about merged resource dictionaries.
The last fundamental piece of the puzzle is the mechanism by whichvisual resources are dynamically associated with properties ofelements. This is where the DynamicResource markupextension comes into play. A dynamic resource reference is similar to adata binding in that, when the resource is replaced at runtime, theproperties that consume it will be given the new resource.
For example, suppose we have a TextBlock whose Background property must be set to whatever Brush the current skin dictates it should paint itself with. We can establish a dynamic resource reference for the TextBlock‘s Background property. When the skin changes at runtime, along with the brush to be used by the TextBlock, the dynamic resource reference will automatically update the TextBlock‘s Background to use the new brush. Here is what that looks like in XAML:

Refer to the "External links" section at the end of this article to see how to write that in code.
The resources for each skin should be placed into a separate ResourceDictionary, each of which belonging in its own XAML file. At runtime we can load a ResourceDictionary that contains all of the resources for a skin -- hereafter referred to as a "skin dictionary" -- and insert it into the MergedDictionaries of the Application‘s ResourceDictionary. By placing a skin dictionary into the Application‘s resources, all elements in the application will be able to consume the resources it contains.
All of the elements in the UI that must support skinning shouldreference the skin resources via dynamic resource references. Thisallows us to change the skin at runtime and have those elements use thenew skin resources.
The easiest way to accomplish this is to have an element‘s Style property assigned to a dynamic resource reference. By using an element‘s Style property, we allow the skin dictionaries to contain Stylesthat can set any number of properties on the skinned elements. This ismuch easier to write and maintain than setting dynamic resourcereferences on every single property that gets its value from a skindictionary.
The demo application, which can be downloaded at the top of this article, contains one simple Windowthat can be skinned in three ways. That is, unless you decide to createmore skins. When you first run the application, it uses the defaultskin, which looks like this:

If you right-click anywhere on the Window, a ContextMenu pops open, allowing you to change the skin. Here is what that looks like:

In a real application, this would definitely be a grotesque way ofallowing the user to choose a skin, but this is just a demo app! If theuser were to click on the agent named David in the ListBox and then select the green bar in the ContextMenu, the "Green Skin" would be applied and the UI would look like this:

Note: The fact that the selected agent‘s last name is Greene has nothing to do with the fact that the UI is now green! :)
The last skin I created is a little weird, but I like it. Here‘s what the UI looks like when the "Blue Skin" is applied:

As you can probably tell, I‘m not a very good visual designer.
Here is the demo project structure, as seen in Visual Studio‘s Solution Explorer:

ContextMenu, which allows the user to change the active skin, is declared in the MainWindow XAML file like so:



















When the user selects a new skin in the menu, this code executes in MainWindow‘s code-behind file:
void OnMenuItemClick(object sender, RoutedEventArgs e)
{
MenuItem item = e.OriginalSource as MenuItem;
// Update the checked state of the menu items.
Grid mainGrid = this.Content as Grid;
foreach (MenuItem mi in mainGrid.ContextMenu.Items)
mi.IsChecked = mi == item;
// Load the selected skin.
this.ApplySkinFromMenuItem(item);
}
void ApplySkinFromMenuItem(MenuItem item)
{
// Get a relative path to the ResourceDictionary which
// contains the selected skin.
string skinDictPath = item.Tag as string;
Uri skinDictUri = new Uri(skinDictPath, UriKind.Relative);
// Tell the Application to load the skin resources.
DemoApp app = Application.Current as DemoApp;
app.ApplySkin(skinDictUri);
}
The call to ApplySkin on the DemoApp object results in this method to be executed:
public void ApplySkin(Uri skinDictionaryUri)
{
// Load the ResourceDictionary into memory.
ResourceDictionary skinDict =
Application.LoadComponent(skinDictionaryUri) as ResourceDictionary;
Collection mergedDicts =
base.Resources.MergedDictionaries;
// Remove the existing skin dictionary, if one exists.
// NOTE: In a real application, this logic might need
// to be more complex, because there might be dictionaries
// which should not be removed.
if (mergedDicts.Count > 0)
mergedDicts.Clear();
// Apply the selected skin so that all elements in the
// application will honor the new look and feel.
mergedDicts.Add(skinDict);
}
Now we will take a look at an example of how elements in the UIconsume the skin resources. The following XAML represents the "Agents"area on the left side of the MainWindow. It contains a ListBox full of insurance agent names and a header which reads "Agents."
Collapse
x:Class="SkinnableApp.AgentSelectorControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>









Source=".\Resources\Icons\agents.ico" />
VerticalAlignment="Center" />



Grid.Row="1" IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding}"
ItemTemplate="{DynamicResource agentListItemTemplate}"
ScrollViewer.HorizontalScrollBarVisibility="Hidden" />



Here is what that AgentSelectorControl looks like when the default skin is applied:

There are three uses of the DynamicResource markup extension in the AgentSelectorControlseen above. Each of them refers to a resource that must exist in a skindictionary. All of the skin dictionaries are available in the demoproject, so I won‘t bother bloating this article with gobs ofmarginally interesting XAML.