Sunday, April 25, 2010

A Chrome and Glass Theme - Part 4

[UPDATED]: Fixed the links to the final source.

So back to the series on building a theme. Apart from describing the steps to style each of the controls for this particular theme, I've been trying to describe some of the techniques and knowledge that can be applied generally to any theme or style.
  • In part 1 we covered creating style resources in a resource dictionary.
  • In part 2 we covered editing control templates as part of a style, including states.
  • In part 3 we knocked off a couple of the easier controls to style, although we added some extra elements to the control template.
In this post, we are going to dig down into a control that is made up of a collection of other controls: the ListBox. But we are going to begin by styling the other controls that make up the ListBox. If you haven't been following along, grab the files from the end of part 3 here and load them up in Blend. This is what the final result will look like (with sample data):


Parts of a ListBox
A list box has a number of templates that it uses and it's important to get a good understanding of how it hangs together. Here is the break-down:



A list box has a control template that contains a ScrollViewer control. A ScrollView control has a control template that contains two ScrollBar controls. And the ScrollBar controls have their own template as well (which is divided up into horizontal and vertical elements).

Additionally, the ListBox uses two other templates (there is a third that is not part of the reusable style that we will discuss briefly at the end): another control Template (inside the ListBox.ItemContainerStyle) that determines what the items in the ListBox look like, and an ItemsPanelTemplate (ListBox.ItemsPanel) that determines how the items are laid out (eg horizontally, vertically etc).

When you are looking at a styled ListBox, the items in the list look the way they do because of the ListBox.ItemContainerStyle template, which is a ControlTemplate. They are laid out the way they are (usually vertically) because of the ListBox.ItemsPanel which is an ItemsPanelTemplate that contains a stack panel by default, and they are "framed" the way they are (the background, scrollbars etc) because of the ListBox.Template which is a ControlTemplate. We won't bother changing the ListBox.ItemsPanel in this post since we still want the behavior that we get with the default vertical StackPanel.

The ScrollBar
It's easy to get lost in template editing if you dig down and down through the control templates. We are going to approach things in a fairly simple-to-follow way by styling the resources we need from the innermost controls, and work our way back up to the top level control. The first control, buried deep inside the ListBox control, is a ScrollBar. Strangely enough, the ScrollBar is probably the most time consuming of the controls we will style even though it seems a simple control. You'll see why as we go along.

Drag a scroll bar onto our glassy panel - it will be vertical by default. Use the breadcrumb toolbar to edit a copy of the control tempate; call it ScrollBarChromeStyle and remember to save it in our ChromeGlass.xaml resource dictionary.

The template doesn't have any size defined, but Blend allows us to define a Design-Time only size. Select the "Root" element in the Object tree and use the bottom blue drag handle to make our scroll bar a bit taller.

If you expand the "VerticalRoot" element you will see that the scroll bar is made up from three rectangles and five controls. We are only going to need two of those rectangles, so delete the second one in the Object tree under the "VerticalRoot" grid. Set the Fill of the first Rectangle to our GlassFill resource brush, and the stroke of the second Rectangle to the "ChromeBorder" resource brush we defined in Part 1.

Select the "VerticalSmallDecrease" element - it's a RepeatButton control, as are the other small and large decrease buttons. Use the breadcrumb toolbar to edit the current control template. The Repeat button is a specialized kind of button that does not contain any content of its own (like a regular Button does), but still has button states. Select the "Background" rectangle element, change its Fill brush to the "ChromeGlass" resource brush, it's Stroke to empty, and its Opacity to 100%.

Select the "BackgroundMouseOver" Rectangle element, change its Fill to empty, and its Stroke brush to the "ChromeBorder" resource brush.

Select the "BackgroundGradient" Rectangle element, drag it up to be above the "BackgroundPressed" Rectangle in the Object tree, and change to following properties (don't worry about any warnings of animations being removed):
  • Set its Fill to the "ChromeFill" resource brush
  • Set its Stroke to the "ChromeBorder" resource brush
  • Set its Margin to 2 for all edges

Select the "BackgroundPressed" Rectangle element and change its Margin values to 2 for all edges. Select the "Highlight" Rectangle element and change its Margin to 0 for all edges.

Finally, select the unnamed path element and change it's foreground to White. Now to set the values for the different states.

Select the "MouseOver" state (in the "States" tab) and change the Opacity of the "Background" Rectangle to 0%. Select the "Pressed" state and set the Opacity for the "Background" element to 0%, the "BackgroundGradient" element to 100%, the "BackgroundPressed" element to 20%, and the "Highlight" element to 20%.

The good news is, that the VerticalSmallDecrease element is now styled. The bad news is, that we have to repeat this process for the VerticalSmallIncrease the HorizontalSmallDecrease, and the HorizontalSmallIncrease. Since the VerticalSmallIncrease element is essentially the same as the VerticalSmallDecrease element, I won't bother repeating the same instructions. Go ahead and follow the same instructions for the VerticalSmallIncrease RepeatButton; I'll wait for you...

...and we're back. Now lets style the "VerticalThumb" element. Select it and use the breadcrumb toolbar to edit the current template. You should see it's made up of nothing more than five Rectangle elements. Here we go (you may wish to select the top Grid element and use the Design Time size handles so you can see what the changes look like):

Change the "Background" element's Fill brush to empty, and its Stroke to the "ChromeBorder" resource.

Move the "BackgroundGradient" element up under the "Background" element, change its Fill to the "ChromeFill" resource and its Stroke to the "ChromeBorder" resource, and set its margin to 2 for all edges.

Change the "BackgroundMouseOver" element's Fill to White, its stroke to empty, and it's Margin to 2 for all edges.

Change the "Highlight" element's Margin to 0 for all edges.

Select the "MouseOver" state, and change the "BackgroundMouseOver" element's Opacity to 20%. Select the "Pressed" state and change the "BackgroundPressed" element's Opacity to 30% and the "Highlight" element's Opacity to 30%.

That's the vertical layout elements styled; now we have to do the horizontal layout elements. Normally, the easiest way to style the horizontal layout would be to click the "[scrollbar]" button on the breadcrumb trail and change the Orientation of the ScrollBar to Horizontal (you will want to resize it too), and then use the breadcrumb trail to go back into the template and expand the "HorizontalRoot" grid element.

However, at this point Blend seems determined to make things hard for us; it insists on only showing the design template for the vertical orientation regardless of the value of the Orientation property. So we are going to have to do this blind, but we will be guided by what we have done with the vertical scrollbar. You can also switch back and forth between "ChromeGlass.xaml" and "MainPage.xaml" to see the results of your changes. If we had chosen to save the template in "MainPage.xaml" instead of our resource dictionary then we would have been editing "in-place" and would be able to see our changes reflected in the design surface. We probably should have done that, and then moved the template into the resource dictionary when we had finished.

Here are the steps for the "HorizontalRoot" template:

Horizontal Root
  • Delete the second Rectangle element - we don't need it, there should be three left.

  • Set the Fill brush for the first Rectangle to the "GlassFill" resource brush.


HorizontalSmallDecrease
  • Select the "HorizontalSmallDecrease" element and use the breadcrumb trail to edit it's template.
  • Select the "Background" Rectangle element and set its Fill to the "ChromeGlassFill" resource brush, its stoke to "No brush", and its Opacity to 100%.
  • Select the "BackgroundMouseOver" Rectangle element and set its Fill to "No brush" and it's Stroke to the "ChromeBorder" resource brush.
  • Select the "BackgroundPressed" Rectangle element and set its Margin to 2 for all edges.
  • Select the "BackgroundGradient" Rectangle, drag it up in the Objects tree to be above the BackgroundPressed Rectangle, set its Fill to the "ChromeFill" resource brush, its Stroke to the "ChromeBorder" resource brush, and it's Margin to 2 for all edges.
  • Select the "MouseOver" state and set the Opacity of the "Background" Rectangle to 0%, the "BackgroundMouseOver" Rectangle to 100%, and the "BackgroundGradient" Rectangle to 100%.
  • Select the "Pressed" state and set the Opacity of the "Background" Rectangle to 0%, the "BackgroundGradient" Rectangle to 100%, the "BackgroundPressed" Rectangle to 20%, and the "Highlight" Rectangle to 30%.
  • Select the "Highlight" Rectangle element and set its Margin to 0 for all edges.
  • Select the "[Path]" element and set it's Fill to a solid white color.


HorizontalSmallIncrease
Use the breadcrumb trail to go back out to the scrollbar template, select the "HorizontalSmallIncrease" element. For some strange reason, this one template is implemented differently by default than the other 3 small increase RepeatButton elements in the ScrollBar template. There is no "BackgroundPressed" Rectangle element, so select the "BackgroundGradient" Rectangle, press Ctrl+C followed by Ctrl+V to create a copy, select the copy, click its name once and rename it to "BackgroundPressed". Drag it up so it sits just below the "BackgroundGradient" Rectangle, and set its Fill to a solid color with the value "#FF448DCA", its stroke to "#00000000", and its margin to 2 for all edges. Also change the name of the "BackgroundAnimation" Rectangle to "BackgroundMouseOver". You can now follow the same steps as for the "HorizontalSmallDecrease" element, with one additional step: in the "Pressed" state, you will have to set the Opacity for the (now named) "BackgroundMouseOver" element to 0%.

HorizontalThumb
You can follow the same steps for the "VerticalThumb" element to style the "HorizontalThumb".



That's the hard work done, the rest of the ListBox template styling goes quickly now.

The ScrollViewer
Go back out to MainPage.xaml design surface and place a ScrollViewer control inside the grid containing our other controls. The ScrollViewer isn't available on the control toolbar fly-outs so you can either use the ">>" chevron at the bottom of the toolbar to list all controls and find the ScrollViewer in there, or you can select the "Assets" tab and enter "scroll" into the search box and Blend will filter all available controls that contain the search term:


Once you have created the scroll viewer, use the breadcrumb trail to edit a copy of its template, saving it as "ScrollViewerChromeStyle" in the "ChromeGlass.xaml" resource dictionary.

The only thing we need to change in the template is the style of the scroll bars. We can do that by right-clicking the VerticalScrollBar element, and selecting "Edit Template" | "Apply Resource" | "ScrollBarChromeStyle". Do the same to the HorizontalScrollBar. Now select the "[Rectangle]" element and delete it. That's it for the template, we just need to set the border brush on the style resource.

Click the artist's palette on the breadcrumb toolbar to edit the style of the ScrollViewer and change it's Border to the "ChromeBorder" resource brush. We also want to have the inside of the ScrollViewer subtly differentiated from the outside of it so set its Background brush to a "#26000000" Solid brush. The style may show the foreground as Black, but since we have applied a White Foreground to our UserControl, the foreground will default to White when we actually use the control in our MainPage.xaml.

That's the ScrollViewer done. Here is what it looks like on our main page, with a couple of Scrollbars too:


We could have used the "GlassFill" resource brush for the background of the scrollable area, but the ScrollViewer is going to be used in the ListBox template, and the angled shine of the Glassfill brush would end up looking odd with multiple ListBox elements of different sizes on the same screen, since the shine would be at different angles depending on the size of the ListBox.

The Listbox Control Template
Finally, we get to the ListBox control. There are two templates we need to edit here, the control template for the ListBox, and the ListBoxItem template that will hold each item in the ListBox.

You can delete the ScrollViewer control on the MainPage.xaml screen and replace it with a ListBox control. Create a new resource template for the ListBox called "ListBoxChromeStyle" in the "ChromeGlass" resource dictionary.

Expand the template in the Objects tree and right click the "ScrollViewer" element. Select "Edit Template" | "Apply Resource" | "ScrollViewerChromeStyle". You will see that the background is still white from the default template because the Background brush is template-bound to the parent Background brush for the ListBox control. We could reset the Background brush for the ScrollViewer, but that would mean that any changes we make to the Background brush would be ignored for individual ListBox controls that use this style. So select the artists palette on the Breadcrumb toolbar for the actual Style of the ListBox and set the Background to a solid color of "#22000000" - the same that we used for the ScrollViewer style template. The Border brush of the "[Border]" element in the ListBox template is also template-bound to the style, so change the style's BorderBrush to the "ChromeBorder" resource brush. The Foreground color of the parent control is not passed onto child ListBoxItems, so set the Foreground of the ListBox style to white.

That's the main chrome of the ListBox done, so now lets do the ItemContainerStyle.

The ItemContainerStyle
Go back out to the MainPage.xaml view and select the ListBox. Use the breadcrumb trail to edit, under "Edit Additional Templates", a new copy of the "Generated Item Container". Call it "ListBoxItemChromeStyle" and save it in the resource dictionary.

Select the "fillColor" Rectangle element and change its Fill the the "ChromeGlassFill" resource brush. Select the "fillColor2" Rectangle element and change its Fill also to the "ChromeGlassFill" resource brush, and it's Stroke to the "ChromeBorder" resource brush. Select all three Rectangle elements by holding Ctrl and clicking on each one, and change the RadiusX and RadiusY values to 3.

Now select the "MouseOver" State, and set the Opacity of the "fillColor" Rectangle to 50%. Select the "Focused" State and set the Opacity of the "FocusVisualElement" Rectangle to 30%. Select the "Selected" State and set the Opacity of the "fillColor2" Rectangle to 100%. Finally, select the "Disabled" State, delete the Opacity adjustment for the "contentPresenter" element and set the Opacity of the "grid" element to 55%.

One thing that has always annoyed me about the list box is that the default template for the ListBoxItem has the HorizontalAlignment set to Left - which means that the selected item highlight doesn't stretch across the whole ListBox. To change this, select the "contentPresenter" element and use the Advanced Property Options box to reset it's HorizontalAlignment, and then change it to Stretch instead of Left.

That was the last part of the reusable ListBox template that we needed to style, but there is one more thing to do. Currently, the ListBox control that we created has its "ItemContainerStyle" property set to the "ListBoxItemChromeStyle" resource that we just created. But we really want this to be set on the "ListBoxChromeStyle" style resource rather than on the ListBox itself. So select the ListBox, and from the "Object" menu, select "Edit Style" | "Edit Current" to edit the "ListBoxChromeStyle" resource. Expand the "Miscellaneous" group heading and, from the advanced property options box on the right hand side of the "ItemContainerStyle" property, select the local resource "ListBoxItemChromeStyle". Finally, exit out of editing the template and, back on the ListBox itself, use the advanced property options to reset the value of the ListBox's "ItemContainerStyle" property. So now, whenever we use our "ListBoxChromeStyle" resource, it comes already connected to the "ListBoxItemChromeStyle" resource.

That is the last thing we needed to do to our ListBox style, so now lets add some sample data to test it with.

Adding Sample Data
Blend has built in functionality to create sample data that we can use when designing our layouts to test how those layouts work with actual data.


Select the "Data" tab and click the "Add sample data source" button. Select the "Define New Sample Data..." option and accept the default Data source name "SampleDataSource", creating it in the Project, not the Document.

Blend will have created two fields for you by default. You can use the little arrow on the right edge of the property to change what kind of sample data is generated. Rename the two fields "Name" and "Company" and add a third one named "Image". Use the "Change Property type" arrows to select the right kind of data to be generated for each field.

To bind the generated data to your ListBox, click and drag the "Collection" item in the SampleDataSource tree onto the ListBox. Blend will automatically create a data template for it and show the data stacked vertically in a StackPanel control, but we're going to change that now.

Editing the ItemTemplate
The last template is the Listbox.ItemTemplate. It is a DataTemplate that will be applied to the data inside each ListItemBox. It has nothing to do with our reusable "ListBoxChromeStyle" resource, and is something that you customize specifically for each ListBox you use. I'm covering it briefly here just for the sake of putting all the ListBox templates in context.

Use the Breadcrumb toolbar to edit the current "Generated Items (ItemTemplate)". I'm not going to give any instructions on what to do here since it is unrelated to our style sheet. Just find a nice layout for the items that are there, or grab my one using the link at the end of this post. You will notice that you are editing this template "in-place" since it was created inside the MainPage.xaml file, rather than the resource dictionary.

So here are the things we have covered in this post:
  • It's easier, with complex controls, to style the child controls that it uses first.
  • The different templates that make up the list box and how they relate to each other.
  • How to create reusable styles and templates for each of them.
  • The ListBox.ItemTemplate is specific to each instance of a ListBox, so it's not part of our resource dictionary.

You can grab the final solution files here.

5 comments:

  1. Great series of posts!

    I did try to grab the final solution using the link at the bottom of this but I am getting a 404. Could you please look into this? It would be really helpful to have this when I go throught the posts again in more detail.

    Thanks!!!

    ReplyDelete
  2. I had trouble with the link to gettig the final solution. I would love to be able to look at the source.

    ReplyDelete
  3. Links should be working now. Sorry about that.

    ReplyDelete
  4. Very nice series. I have been doing all my styles, themes, and templates just by editing XAML in Dev Studio, a lot of trial and error. Learning to use Blend has seemed like a daunting "wish list" task until I stumbled upon your blog. Thank you.

    ReplyDelete
  5. Thanks for the great feedback guys.

    ReplyDelete