Skip to content

Instantly share code, notes, and snippets.

@mgaffigan
Last active March 8, 2025 19:03
Show Gist options
  • Select an option

  • Save mgaffigan/e74140df7518532e5ef07c4894aa0321 to your computer and use it in GitHub Desktop.

Select an option

Save mgaffigan/e74140df7518532e5ef07c4894aa0321 to your computer and use it in GitHub Desktop.
WPF OutlookBar panel
<Window x:Class="OutlookBarPanelTest.MainWindow" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:OutlookBarPanelTest"
mc:Ignorable="d"
Title="MainWindow" Height="500" Width="300">
<Window.Resources>
<Style x:Key="OutlookBarItemBorderStyle" TargetType="Border">
<Setter Property="BorderBrush" Value="#8492A6" />
<Setter Property="BorderThickness" Value="0,0,0,1" />
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
<GradientStop Color="#EEF5FD" Offset="0.0"/>
<GradientStop Color="#E8F1FB" Offset="0.4"/>
<GradientStop Color="#DCE6F4" Offset="0.4"/>
<GradientStop Color="#DDE9F7" Offset="0.4"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="FocusVisual">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle Margin="2" StrokeDashArray="1 2" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" SnapsToDevicePixels="true" StrokeThickness="1"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="OutlookBarTabControlItemContainerStyle" TargetType="{x:Type local:TabItemWithIcon}">
<Setter Property="FocusVisualStyle" Value="{StaticResource FocusVisual}"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TabItemWithIcon}">
<Border x:Name="mainBorder" SnapsToDevicePixels="true" Style="{StaticResource OutlookBarItemBorderStyle}">
<Grid x:Name="contentPresenter">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Source="{TemplateBinding Icon}" SnapsToDevicePixels="True" Height="16" Width="16" VerticalAlignment="Center" HorizontalAlignment="Center" />
<ContentPresenter Grid.Column="1" ContentSource="Header" Focusable="False" VerticalAlignment="Center" />
</Grid>
</Border>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource Mode=Self}}" Value="true">
<Setter Property="Background" TargetName="mainBorder">
<Setter.Value>
<LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
<GradientStop Color="#F5D4AD" Offset="0"/>
<GradientStop Color="#FDCE87" Offset="0.35"/>
<GradientStop Color="#FFCC6A" Offset="0.4"/>
<GradientStop Color="#FFCC6A" Offset="0.6"/>
<GradientStop Color="#FEF4AE" Offset="1.0"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding IsMouseOver, RelativeSource={RelativeSource Mode=Self}}" Value="true">
<Setter Property="Background" TargetName="mainBorder">
<Setter.Value>
<LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
<GradientStop Color="#F5D4AD" Offset="0.0"/>
<GradientStop Color="#FDCE87" Offset="1.0"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding IsEnabled, RelativeSource={RelativeSource Mode=Self}}" Value="false">
<Setter Property="Opacity" TargetName="contentPresenter" Value="0.56"/>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="OutlookBarTabControl" TargetType="{x:Type TabControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabControl}">
<Grid x:Name="templateRoot" ClipToBounds="true" SnapsToDevicePixels="true" KeyboardNavigation.TabNavigation="Local">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="7" />
<RowDefinition Height="Auto" MaxHeight="160" MinHeight="32" />
</Grid.RowDefinitions>
<Border Grid.Row="0" KeyboardNavigation.DirectionalNavigation="Contained" KeyboardNavigation.TabIndex="2" KeyboardNavigation.TabNavigation="Local">
<ContentPresenter x:Name="PART_SelectedContentHost" ContentSource="SelectedContent" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
<GridSplitter Grid.Row="1" ResizeDirection="Rows" Background="DarkGray" HorizontalAlignment="Stretch" DragIncrement="32" />
<local:OutlookBarPanel Grid.Row="2" Background="Transparent" IsItemsHost="true" KeyboardNavigation.TabIndex="1" IconicHeight="32" FullItemHeight="32" Panel.ZIndex="1"/>
<Border Grid.Row="2" Height="32" VerticalAlignment="Bottom" Style="{StaticResource OutlookBarItemBorderStyle}" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="TextElement.Foreground" TargetName="templateRoot" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemContainerStyle" Value="{StaticResource OutlookBarTabControlItemContainerStyle}" />
</Style>
</Window.Resources>
<TabControl Style="{DynamicResource OutlookBarTabControl}" Grid.Row="2" x:Name="ic" />
</Window>
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
namespace OutlookBarPanelTest;
internal partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var resourceNames = typeof(MainWindow).Assembly.GetManifestResourceNames();
foreach (var resourceName in resourceNames)
{
if (!resourceName.EndsWith(".png")) continue;
ic.Items.Add(new TabItemWithIcon
{
Header = resourceName.Replace("OutlookBarPanelTest.Icons.", "").Replace(".png", ""),
Icon = BitmapSourceFromResourceName(resourceName),
Content = new TextBlock { Text = resourceName }
});
}
}
private static BitmapSource BitmapSourceFromResourceName(string name)
{
var asm = typeof(MainWindow).Assembly;
using var stream = asm.GetManifestResourceStream(name);
var decoder = BitmapDecoder.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
return decoder.Frames[0];
}
}
internal class TabItemWithIcon : TabItem
{
public BitmapSource Icon
{
get { return (BitmapSource)GetValue(IconProperty); }
set { SetValue(IconProperty, value); }
}
public static readonly DependencyProperty IconProperty =
DependencyProperty.Register(nameof(Icon), typeof(BitmapSource), typeof(TabItemWithIcon));
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace OutlookBarPanelTest;
// Act like a stack panel arranging children in a vertical stack from bottom to top
// When no more space is left to fit a full child, stack the remainder from right
// to left along the bottom
//
// Take all width available, but only the height of the full items + the iconic height
internal class OutlookBarPanel : Panel
{
#region Attached property for children
public static bool GetIsIconic(DependencyObject obj) => (bool)obj.GetValue(IsIconicProperty);
private static readonly DependencyPropertyKey IsIconicPropertyKey =
DependencyProperty.RegisterAttachedReadOnly("IsIconic", typeof(bool), typeof(OutlookBarPanel), new PropertyMetadata(false));
public static readonly DependencyProperty IsIconicProperty = IsIconicPropertyKey.DependencyProperty;
#endregion
public double FullItemHeight
{
get { return (double)GetValue(FullItemHeightProperty); }
set { SetValue(FullItemHeightProperty, value); }
}
public static readonly DependencyProperty FullItemHeightProperty =
DependencyProperty.Register(nameof(FullItemHeight), typeof(double), typeof(OutlookBarPanel),
new FrameworkPropertyMetadata(50d, FrameworkPropertyMetadataOptions.AffectsMeasure));
public double IconicHeight
{
get { return (double)GetValue(IconicHeightProperty); }
set { SetValue(IconicHeightProperty, value); }
}
public static readonly DependencyProperty IconicHeightProperty =
DependencyProperty.Register(nameof(IconicHeight), typeof(double), typeof(OutlookBarPanel),
new FrameworkPropertyMetadata(16d, FrameworkPropertyMetadataOptions.AffectsMeasure));
private int _fullItemCount;
private bool _hasIconic;
protected override Size MeasureOverride(Size availableSize)
{
if (double.IsFinite(availableSize.Height))
{
_fullItemCount = (int)Math.Floor(availableSize.Height / FullItemHeight);
_hasIconic = _fullItemCount < InternalChildren.Count;
// If we need to show icons, we might need to show one fewer full item
// in order to have enough space for them.
if (_hasIconic
&& _fullItemCount > 0
&& (_fullItemCount * FullItemHeight + IconicHeight) > availableSize.Height)
{
_fullItemCount -= 1;
}
}
else
{
_fullItemCount = InternalChildren.Count;
_hasIconic = false;
}
// Measure all children
var maxWidth = 0d;
foreach (var child in InternalChildren.Cast<UIElement>().Take(_fullItemCount))
{
child.SetValue(IsIconicPropertyKey, false);
child.Measure(new Size(availableSize.Width, FullItemHeight));
maxWidth = Math.Max(maxWidth, child.DesiredSize.Width);
}
foreach (var child in InternalChildren.Cast<UIElement>().Skip(_fullItemCount))
{
child.SetValue(IsIconicPropertyKey, true);
child.Measure(new Size(IconicHeight, IconicHeight));
maxWidth = Math.Max(maxWidth, child.DesiredSize.Width);
}
var height = _fullItemCount * FullItemHeight;
if (_hasIconic) height += IconicHeight;
return new Size(maxWidth, height);
}
protected override Size ArrangeOverride(Size finalSize)
{
var fullItemRect = new Rect(0, 0, finalSize.Width, FullItemHeight);
var iconicRect = new Rect(finalSize.Width, finalSize.Height - IconicHeight, IconicHeight, IconicHeight);
foreach (var child in InternalChildren.Cast<UIElement>().Take(_fullItemCount))
{
child.Arrange(fullItemRect);
fullItemRect.Y += FullItemHeight;
}
foreach (var child in InternalChildren.Cast<UIElement>().Skip(_fullItemCount))
{
iconicRect.X -= IconicHeight;
child.Arrange(iconicRect);
}
return finalSize;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment