Last active
March 8, 2025 19:03
-
-
Save mgaffigan/e74140df7518532e5ef07c4894aa0321 to your computer and use it in GitHub Desktop.
WPF OutlookBar panel
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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> | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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