Showing posts with label WPF. Show all posts
Showing posts with label WPF. Show all posts

Saturday, 22 January 2011

An Expression Blend TabControl Style

After spending some time trawling the web and failing to find the style I was after, I decided to sit down and put one together myself.

A couple of hours later, and with a bit of help from Snoop, this is what I came up with; It’s a style for a tab control and, in-particular, a tab item, that goes some way to looking and feeling like the one used by Expression Blend.


The tabs are prevented from rearranging themselves into multiple lines by swapping out the TabPanel with a StackPanel.

To more closely resemble the style adopted by Blend the TabPanel could instead be replaced with the SqueezeTabPanel, which can be found on CodeProject. For the time being, this has been left as an exercise for the reader ;)

XAML:
 <Style TargetType="{x:Type TabControl}">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid KeyboardNavigation.TabNavigation="Local">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel
Orientation="Horizontal"
Name="HeaderPanel"
Grid.Row="0"
Panel.ZIndex="1"
Margin="0,0,4,-1"
IsItemsHost="True"
KeyboardNavigation.TabIndex="1"
Background="Transparent" />
<Border
Name="Border"
Grid.Row="1"
Background="LightGray"
CornerRadius="2"
KeyboardNavigation.TabNavigation="Local"
KeyboardNavigation.DirectionalNavigation="Contained"
KeyboardNavigation.TabIndex="2" >
<ContentPresenter
Name="PART_SelectedContentHost"
Margin="4"
ContentSource="SelectedContent" />
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="#888888" />
<Setter TargetName="Border" Property="BorderBrush" Value="#AAAAAA" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type Button}">
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Grid>
<Path
x:Name="Cross"
Data="M0,0L6,6 M6,0L0,6z"
Stroke="LightGray"
StrokeThickness="1.7"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter TargetName="Cross" Property="Stroke" Value="White"/>
</Trigger>
<Trigger Property="IsPressed" Value="true">
<Setter TargetName="Cross" Property="Stroke" Value="LightGray"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="#888888"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Grid Margin="0,0,-12,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="24"/>
</Grid.ColumnDefinitions>
<Border
x:Name="Border"
Grid.Column="0"
Grid.ColumnSpan="2"
CornerRadius="3,0,0,0"
Background="#FF333333" />
<Path
x:Name="RoundedCorner"
Grid.Column="2"
Stretch="Fill"
Data="M6.5,2.6C4.767,0.973 2.509,0 0,0 0,0 0,19 0,19L23,19z"
Fill="#FF333333" />
<Button
x:Name="CloseButton"
Grid.Column="1"
Visibility="Collapsed"
Margin="0,0,0,0"
Width="6"
Height="6"/>
<ContentPresenter
x:Name="ContentSite"
TextElement.Foreground="White"
VerticalAlignment="Center"
HorizontalAlignment="Center"
ContentSource="Header"
Margin="12,2,12,2"
RecognizesAccessKey="True"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Panel.ZIndex" Value="100" />
<Setter TargetName="Border" Property="Background" Value="Gray" />
<Setter TargetName="RoundedCorner" Property="Fill" Value="Gray" />
<Setter TargetName="CloseButton" Property="Visibility" Value="Visible" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Border" Property="Background" Value="#EEEEEE" />
<Setter TargetName="RoundedCorner" Property="Fill" Value="#EEEEEE" />
<Setter Property="Foreground" Value="#888888" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

Thursday, 21 January 2010

WPF Header/Multi-Column ListBox Control



The WPF ListBox control can be used to create a wide variety of user interface elements thanks to templates and styling. However, recently it left me out in the cold...for a while at least.

We had an existing control, based on a heavily styled ListBox, that contained a bunch or items that could be considered as rows in a table. It all worked fine until some bright spark had the idea of adding column headers.

Now, at this point you may be thinking that ListBox is not the ideal candidate here. ListView, with its GridView layout, is the control to use if you want to layout column based data with column headers. Right?

Well, not always. GridView is fine if you want to apply styles to individual cells or columns. The problem with our particular style was that each conceptual row could be expanded in various ways, making the GridView unsuitable. There were two options as far as i could see:

  1. Write a custom View (based on ViewBase) for ListView
  2. Add a header to the ListBox control

We went with option two. The pain began.

The basic problem is that the header needs to be part of the ListBox so that when the ListBox is scrolled horizontally, the header scrolls too. However, when you scroll it Vertically you want the header to remain visible and not scroll with the rest of the list.

I started by looking at the style of the ListView control which does something similar to what i wanted to achieve. This gave me a big clue. The ListView uses a custom ScrollViewer that has a child scrollviewer nested inside. This child ScrollViewer contains the header; The scrollbars on the child ScrollViewer are always hidden. Magic.

I also had a look at this very helpful article: Building a Multi-Column ListBox in Avalon

This came close to what i wanted to achieve, but there were a number of problems:

  • It was written before the first release of WPF and would no longer compile
  • Having updated it so that it did compile, there were several problems with the scrolling resulting in unsatisfactory placement of the vertical scrollbar (not consistent with ListView) and errors causing the content to slide past the header at the extreme right.

Nevertheless, it gave me a lot of ideas, In-fact the example is based on a lot of the same XAML.

Next, I need to work out how to make the header scroll horizontally when the list box is scrolled horizontally. Unfortunately there is no easy way to do this. Ideally you would bind the HorizontalOffset of the header ScrollViewer to that of the main one. However, this is not possible thanks to its read only nature. To overcome this I used an attached behaviour to achieve the same goal by making use of the ScrollToHorizontalOffset method.

The resulting XAML for the ScrollViewer looks like this:

 <Style x:Key="HeaderedScrollViewer" TargetType="{x:Type ScrollViewer}"> 
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ScrollViewer}">
<Grid Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<DockPanel Margin="{TemplateBinding Padding}">
<ScrollViewer DockPanel.Dock="Top"
local:SetHorizontalOffset.Offset="{Binding
RelativeSource={RelativeSource TemplatedParent},
Path=HorizontalOffset}"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden"
Focusable="false"
Content="{StaticResource ListHeader}">
</ScrollViewer>
<ScrollContentPresenter Name="PART_ScrollContentPresenter"
KeyboardNavigation.DirectionalNavigation="Local"/>
</DockPanel>
<ScrollBar Name="PART_HorizontalScrollBar"
Orientation="Horizontal"
Grid.Row="1"
Maximum="{TemplateBinding ScrollableWidth}"
ViewportSize="{TemplateBinding ViewportWidth}"
Value="{TemplateBinding HorizontalOffset}"
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"/>
<ScrollBar Name="PART_VerticalScrollBar"
Grid.Column="1"
Maximum="{TemplateBinding ScrollableHeight}"
ViewportSize="{TemplateBinding ViewportHeight}"
Value="{TemplateBinding VerticalOffset}"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

And it is applied to the ListBox with this style:

 <Style x:Key="{x:Type ListBox}" TargetType="{x:Type ListBox}"> 
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<ScrollViewer Style="{StaticResource HeaderedScrollViewer}"
Grid.IsSharedSizeScope="True">
<StackPanel IsItemsHost="true"/>
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

You can download the full source here: MultiColumnList.zip

Friday, 6 November 2009

Support for Labels and GDAL Rasters in SharpMap.Wpf

Having finally got around to doing some more work on SharpMap.Wpf I have at last managed to implement generic versions of both the LabelLayer and GdalRasterLayer. As with VectorLayer, a large body of the code is the same as the GDI+ specific versions however they both derive from the new SharpMap.Wpf.Layers.Layer base class which uses an IRenderContext in its overridable Render method instead of a GDI+ graphics object.

There are still a few minor issues to iron out. I haven't gotten around to supporting label rotation yet (although this should not be very difficult) and GdalRasterLayer does not yet do anything with the TransparentColor property. Still, nearly there! Just need a better demo project now.

Some screenshots below:







Tuesday, 22 September 2009

SharpMap.Wpf

I've been wanting to write a WPF renderer for SharpMap ever since I started using version 0.9 about a year ago. Sure, SharpMap v2 has a few early WPF implementations, but its been in beta for quite some time now.

Our existing WPF/SharpMap solution involves using the plain old GDI+ stuff and blatting the entire map into a WPF image using Interop Bitmap. It does the job quite well, so why bother doing anything more?

Well, mainly cos I wanted to! but also because I like the idea of having true WPF styles and themes. The improved brushes in WPF mean that there is scope for some really pretty maps. Also, the idea of using several WPF images to host transparent layers and animated overlays is quite appealing.


So far, I've got a WPF map renderer up and running for vector layers only. I'm currently working on WPFing the LabelLayer and the GdalRasterLayer. Maybe, when its getting to a semi-finished state i'll try and host it on CodePlex. I guess it all depends if anyone is still interested in 0.9 by then!?
A few interesting points on the current state of play:
  • At first I started to write a dedicated WpfVectorLayer and a corresponding WpfVectorRenderer, but I just couldn't live with the code smell. There was too much redundant duplication. In the end, I refactored the VectorLayer into a new generic class with a templatable style and introduced a new Render method that used an IRenderContext instead of a Graphics object. I now have two concrete implementations of IRenderContext, one for GDI+ and one for WPF. To this end, you can declare concrete instances of the generic VectorLayer that either use the standard VectorStyle or the new WpfVectorStyle, depending upon the type of renderer you wish to use.

  • Due to the departure from the original design, all the new stuff is in a separate namespace. The 'legacy' VectorLayer has been left unchanged (yeah, so I've still ended up duplicating code, but at least moving forward you need only implement an IRenderContext)

  • The WPF renderer uses the stream geometry classes to draw straight to a WPF DrawingContext. In addition to this, it provides methods to render the entire map, or just a single layer, to a Visual or a BitmapSource.
Here is my SharpMap.Wpf 'hello world':