Monday 28 September 2009

Styling a WPF Volume Meter

In WPF, the complete separation of behaviour from appearance means that you can take a control such as the ProgressBar, and style it to look like any kind of meter. For a recent project, I wanted to make a audio volume meter. The look I wanted was a gradient bar, starting with green for low volumes and going to red for the highest volume:

wpf-volume-meter-1

When the volume is at half way, the progress bar should just show the left hand portion of the gradient:

wpf-volume-meter-2

Styling a ProgressBar in WPF is actually very easy. You can start with the very helpful “Simple Style” template that comes with kaxaml. You simply need to define the visuals for two parts, called PART_Track and PART_Indicator. The first is the background, while the second is the part that dynamically changes size as the current value changes.

At first it would seem trivial to create the look I am after – just create two rectangles on top of each other. The trouble is, that I only want the whole gradient to appear if the value is at maximum. Here’s it incorrectly drawing the entire gradient when the volume is low:

wpf-volume-meter-3

To work around this, I painted the entire gradient on the PART_Track. Then the PART_Indicator was made transparent. This required one further rectangle to cover up the part of the background gradient that I don’t want. I do this with a DockPanel. This allows the PART_Indicator to use its required space, while the masking rectangle fills up the remaining space on the right-hand side, covering up the background gradient.

<style targettype="{x:Type ProgressBar}" x:key="{x:Type ProgressBar}">
  <setter property="Template">
    <setter.value>
      <controltemplate targettype="{x:Type ProgressBar}">
        <grid minheight="14" minwidth="200">
          <rectangle name="PART_Track" stroke="#888888" strokethickness="1">
            <rectangle.fill>
              <lineargradientbrush startpoint="0,0" endpoint="1,0">
                <gradientstop offset="0" color="#FF00FF00"/>
                <gradientstop offset="0.9" color="#FFFFFF00"/>
                <gradientstop offset="1" color="#FFFF0000"/>
              </lineargradientbrush>
            </rectangle.fill>
          </rectangle>
          <dockpanel margin="1">
            <rectangle name="PART_Indicator">
            </rectangle>
            <rectangle name="Mask" minwidth="{TemplateBinding Width}" fill="#C0C0C0"/>
          </dockpanel>
        </grid>
      </controltemplate>
    </setter.value>
  </setter>
</style>

I think there may be an even better way to solve this using a VisualBrush, but I can’t quite get it working at the moment. I’ll post with the solution once I’ve worked it out.

6 comments:

KenS51 said...

Were you able to get this working. If so can you provide the xaml code. Thanks. Ken

Anonymous said...

Thanks for the tip, spent 20 minutes figuring out how to do it with a visual brush instead of a mask rectangle:

candritzky said...

My first idea was to use a LinearGradientBrush for the PART_Indicator and use an Absolute MappingMode. Then bind the EndPoint.X coordinate to the ActualWidth property of the templated ProgressBar.

Have you tried that already?

Anonymous said...

You can use a linear gradient brush. You just need to set the mapping mode on the gradient brush to absolute. (default is bounding box)

Unknown said...

@Anonymous, the trouble with that is it requires you to know the width of the volume meter

will said...

I used two rectangles. The first one as you did; the whole gradient painted out. The second, a mask box as you had. But I showed more of the color gradient by applying a RenderTransform with the ScaleX property bound to whatever value controls it.
I also had to set flow direction for the mask box from RightToLeft so it would shrink in the correct way to allow more gradient from the bottom verses from the top.