效果图:
控件:支持左边点击,右边内容滚到顶部,右边鼠标中键滚动,左边菜单栏跟着变化。
1.控件样式代码App.xaml
<Application
x:Class="HT.ControlWPF.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:HT.ControlWPF"
StartupUri="MainWindow.xaml">
<Application.Resources>
<Style TargetType="{x:Type local:CustomMenuScrollViewer}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomMenuScrollViewer}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 菜单栏 -->
<ScrollViewer
Grid.Row="0"
Grid.Column="0"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="PART_Mune">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<RadioButton
MinWidth="120"
HorizontalContentAlignment="Stretch"
Command="{Binding CheckedCommand}"
CommandParameter="{Binding ElementName=PART_Function_Content}"
Content="{Binding Title}"
Cursor="Hand"
GroupName="rd"
IsChecked="{Binding IsSeleted, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<RadioButton.Resources>
<Style TargetType="{x:Type RadioButton}">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="10,5,10,5" />
<Setter Property="Margin" Value="2,0,2,0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RadioButton}">
<Border
x:Name="content_Border"
Margin="{TemplateBinding Margin}"
Padding="{TemplateBinding Padding}">
<ContentPresenter
x:Name="contentPresenter"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Focusable="False"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="#379aff" />
</Trigger>
<DataTrigger Binding="{Binding IsSeleted}" Value="True">
<Setter Property="Foreground" Value="#fff" />
<Setter TargetName="content_Border" Property="Background" Value="#379aff" />
<Setter TargetName="content_Border" Property="CornerRadius" Value="0 10 10 0" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</RadioButton.Resources>
</RadioButton>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<!-- 功能内容 -->
<ScrollViewer
x:Name="PART_Function_Content"
Grid.Row="0"
Grid.Column="1">
<ScrollViewer.Resources>
<Style TargetType="Label">
<Setter Property="FontSize" Value="16" />
<Setter Property="Margin" Value="5,2,0,2" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Label}">
<Grid Margin="{TemplateBinding Margin}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Rectangle
Width="4"
Height="18"
Margin="0,0,5,0"
Fill="#379aff" />
<ContentPresenter
Grid.Column="1"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type GroupBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupBox}">
<Grid SnapsToDevicePixels="true">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border x:Name="Header" Grid.Row="0">
<StackPanel>
<ContentPresenter
Name="header"
ContentSource="Header"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
Visibility="Collapsed" />
<Label HorizontalAlignment="Left" Content="{Binding ElementName=header, Path=Content}" />
<Border Height="1" Background="#eeeeee" />
</StackPanel>
</Border>
<ContentPresenter
Grid.Row="1"
Margin="{TemplateBinding Padding}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ScrollViewer.Resources>
</ScrollViewer>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Application.Resources>
</Application>
2.控件业务代码CustomMenuScrollViewer.cs
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace HT.ControlWPF
{
[TemplatePart(Name = PART_Function_Content, Type = typeof(ScrollViewer))]
[TemplatePart(Name = PART_Mune, Type = typeof(StackPanel))]
public class CustomMenuScrollViewer : Control
{
public const string PART_Function_Content = nameof(PART_Function_Content);
public const string PART_Mune = nameof(PART_Mune);
/// <summary>
/// 菜单栏目录
/// </summary>
private List<MuneInfo> muneInfos = new List<MuneInfo>();
/// <summary>
/// 是否加载
/// </summary>
private bool isLoaded = false;
/// <summary>
/// 需要更新菜单
/// </summary>
private bool hasUpdateMune = true;
/// <summary>
/// 需要更新内容 为2则更新
/// </summary>
private int hasUpdateContentOffset = 0;
/// <summary>
/// 内容控件
/// </summary>
private ScrollViewer content_ScrollViewer = null;
/// <summary>
/// 菜单控件
/// </summary>
private ItemsControl mune = null;
static CustomMenuScrollViewer()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomMenuScrollViewer), new FrameworkPropertyMetadata(typeof(CustomMenuScrollViewer)));
}
public override void OnApplyTemplate()
{
content_ScrollViewer = this.GetTemplateChild(PART_Function_Content) as ScrollViewer;
mune = this.GetTemplateChild(PART_Mune) as ItemsControl;
content_ScrollViewer.Content = this.Content;
content_ScrollViewer.Loaded += ScrollViewer_Loaded;
content_ScrollViewer.ScrollChanged += PART_Function_Content_ScrollChanged;
base.OnApplyTemplate();
}
/// <summary>
/// 内容绑定
/// </summary>
[Bindable(true)]
public object Content
{
get { return GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
public static readonly DependencyProperty ContentProperty =DependencyProperty.Register(nameof(Content),typeof(object),typeof(CustomMenuScrollViewer),new FrameworkPropertyMetadata(null));
private void RadioButton_Checked(MuneInfo selectedDataContext)
{
if (!hasUpdateMune) return;
hasUpdateContentOffset = 0;
content_ScrollViewer.ScrollToVerticalOffset(selectedDataContext.StartPoint);
}
private void ScrollViewer_Loaded(object sender, RoutedEventArgs e)
{
if (isLoaded) return;
//找出有Name的GroupBox
var group_boxs = ((Panel)this.content_ScrollViewer.Content).Children.OfType<GroupBox>().Where(s => !string.IsNullOrWhiteSpace(s.Name)).ToList();
for (int i = 0; i < group_boxs.Count; i++)
{
var key = group_boxs[i].Name;
var title = group_boxs[i].Header.ToString();
//计算GroupBox在ScrollViewer的开始位置和结束位置
double endPoint = 0;
double startPoint = 0;
for (int j = 0; j <= i; j++)
{
endPoint += group_boxs[j].ActualHeight;
if (j == i) continue;
startPoint += group_boxs[j].ActualHeight;
}
muneInfos.Add(new MuneInfo()
{
PageKey = key,
IsSeleted = false,
Title = title,
StartPoint = startPoint,
EndPoint = endPoint
});
}
foreach (var item in muneInfos)
{
item.Checked += RadioButton_Checked;
}
mune.ItemsSource = muneInfos;
}
private void PART_Function_Content_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (++hasUpdateContentOffset >= 2)
{
var control = (ScrollViewer)sender;
double offset = control.VerticalOffset;
var itemz = muneInfos.Where(s => s.StartPoint <= offset).OrderByDescending(s => s.StartPoint).FirstOrDefault();
if (itemz != null)
{
hasUpdateMune = false;
itemz.IsSeleted = true;
hasUpdateMune = true;
}
}
}
}
}
3.菜单模型类MuneInfo
using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Windows.Input;
namespace HT.ControlWPF
{
public class MuneInfo : BindableBase
{
public event Action<MuneInfo> Checked;
private bool isSeleted;
/// <summary>
/// 是否选中
/// </summary>
public bool IsSeleted
{
get
{
return isSeleted;
}
set
{
isSeleted = value;
RaisePropertyChanged(nameof(IsSeleted));
}
}
/// <summary>
/// 导航地址
/// </summary>
public string PageKey { get; set; }
/// <summary>
/// 开始位置
/// </summary>
public double StartPoint { get; set; }
/// <summary>
/// 结束位置
/// </summary>
public double EndPoint { get; set; }
private string title;
/// <summary>
/// 标题
/// </summary>
public string Title
{
get
{
return title;
}
set
{
title = value;
RaisePropertyChanged(nameof(Title));
}
}
public ICommand CheckedCommand
{
get
{
return new DelegateCommand(() =>
{
Checked?.Invoke(this);
});
}
}
}
}
4.使用
<Window
x:Class="HT.ControlWPF.MainWindow"
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:local="clr-namespace:HT.ControlWPF"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800"
Height="450"
FontSize="14"
mc:Ignorable="d">
<local:CustomMenuScrollViewer>
<local:CustomMenuScrollViewer.Content>
<StackPanel>
<GroupBox x:Name="groupBox1" Header="功能1标题">
<Border Height="200" />
</GroupBox>
<GroupBox x:Name="groupBox2" Header="功能2标题">
<Border Height="200" />
</GroupBox>
<GroupBox x:Name="groupBox3" Header="功能3标题">
<Border Height="200" />
</GroupBox>
<GroupBox x:Name="groupBox4" Header="功能4标题">
<Border Height="200" />
</GroupBox>
<GroupBox x:Name="groupBox5" Header="功能5标题">
<Border Height="200" />
</GroupBox>
</StackPanel>
</local:CustomMenuScrollViewer.Content>
</local:CustomMenuScrollViewer>
</Window>
注:
1.因为最后那个内容点击第二次才能选中,所以做了一些特殊的处理,还有自己在ItemControl上找不到CheckBox,用了CheckedCommand来触发事件Checked,代码实现有点奇怪,但是功能是已经实现了的,看看那位朋友有好的建议,方便的话和我聊下好的想法。
2.该控件所有代码已经贴出来,可以可以直接使用, 把命名空间改掉就OK。