UPDATE: Now allows more positioning options for Groups and Snapshots (Left, Top, Right, Bottom, Width, Height – all optional).

UPDATE 2: You can now specify rendering and capture DPI values; if you render a 3D scene at a higher DPI (say 300) you’ll get anti-aliased images.  Note you don’t need to do this for 2D content.

You can download the source code for my “SkinBuilder” utility right here.

There have been some enhancements since it was originally posted.  Here’s a typical configuration file (called “SkinBuilder.xml” in my example):

<?xml version="1.0" encoding="utf-8" ?>
<sb:SkinBuilder
    xmlns:sb="http://www.chriscavanagh.com/SkinBuilder"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <sb:Scene>

        <sb:Xaml Source="SkinBuilder.xaml"/>

        <sb:Snapshot Left="0" Top="1" Width="1" Height="173" Filename="background_top_gradient.png" />

        <sb:Group Element="mainPage">

            <sb:Snapshot Left="0" Top="0" Width="14" Height="164" Filename="page_header_lft.png" />
            <sb:Snapshot Left="14" Top="0" Width="748" Height="164" Filename="page_header_ctr.png" />
            <sb:Snapshot Right="0" Top="0" Width="14" Height="164" Filename="page_header_rt.png" />

            <sb:Snapshot Left="0" Top="170" Width="14" Height="1" Filename="main_content_bkgd_lft.png" />
            <sb:Snapshot Left="0" Top="170" Height="1" Filename="main_content_bkgd.png" />
            <sb:Snapshot Right="0" Top="170" Width="14" Height="1" Filename="main_content_bkgd_rt.png" />

            <sb:Snapshot Element="rotatingTest" Storyboard="rotation" Frames="20" Filename="animated-loader.gif" />
            <sb:Snapshot Element="rotatingTest" Storyboard="rotation" Frames="5" Filmstrip="true" Filename="animated-loader-filmstrip.jpg" />

            <sb:Snapshot Left="0" Bottom="0" Height="29" Filename="copyright_bar.png" />

        </sb:Group>

        <sb:Snapshot Filename="Full.jpg" />
        <sb:Snapshot Filename="Full.png" />
        <sb:Snapshot Filename="Full.gif" />

    </sb:Scene>

</sb:SkinBuilder>

There can be multiple <Scene> elements in the configuration file, each one having a XAML reference (or inline XAML content) and any number of <Snapshot> and <Group> elements.  All snapshot origin co-ordinates (Left, Top, Right, Bottom) are optional and relative to their containing groups.  Size values (Width, Height) are optional; where possible they’ll be determined from the container.

The ‘Element’ attribute makes co-ordinates relative to the position of a named XAML element.  In the example above, two snapshots are taken of the “rotatingText” element; because no offset or size is specified, the bounds of the XAML element are used.  If the ‘Element’ attribute is applied to a group, all child snapshots and groups will be relative to it.

If the ‘Storyboard’ attribute is applied to a snapshot, the named WPF storyboard will be recorded.  If the ‘Frames’ attribute is omitted, only the last frame will be captured; otherwise it’ll capture the specified number of frames (including the first and last frame).  By default it will attempt to pass all frames to the BitmapEncoder, but this will only work for formats that support multiple frames (GIF).  Applying the ‘Filmstrip’ (boolean) attribute will generate a single image containing all the frames stacked vertically.  This can be used with CSS’s background-offset property for mouseover effects etc.

In my earlier post I mentioned how your XAML could contain simple data binding expressions; in the sample project I showed how arbitrary properties could be bound to values in an external configuration file.  You could include the configuration file in an ASP.NET project and run SkinBuilder as a post-build step to allow instant re-skinning of a site.  Check out these “themes” I created by single-value tweaks the SiteSpecification.xml file:

Full - Blue Full - Pink Full - Purple Full - Yellow

Finally, sorry for the very brief documentation!  Let me know if I’ve left any obvious holes and I’ll update as necessary🙂