WPF drag过程中显示ToolTip.

在WPF的drag/drop过程中,为了提升用户体验,有时会在鼠标over元素时显示ToolTip以告知用户能否接受拖动内容或元素详情。由于DragOver事件替代了MouseOver,因此需借助Popup实现ToolTip效果。本文介绍了如何通过Xaml数据绑定和ViewModel计算属性,以及在DragOver事件中控制Popup的显示和位置,确保ToolTip在适当时候出现并正确定位。

在drag/drop过程中,我们在判断出over的元素上是否可以接受drag的东西之后,通常是通过鼠标的样式简单告诉用户这个元素不接受现在drag的内容,但是用户通常不知道具体的原因,有时候为了增强客户体验,会在over的过程中显示tooltip来告诉用户为什么不能接受drag的东西或者告诉用户over的元素是什么东西,特别是一些绘图或者其他一些复杂的应用软件,这种方式比较广泛。

WPF 给各种控件提供了ToolTip属性,可以通过设置该属性的值来决定鼠标Over到控件的时候要显示什么内容,但是在Drag的过程中却不能触发MouseOver事件,触发的是DragOver事件,那就需要Popup 来实现该效果,下面是根据自己的经验来简单设计了一个Popup控件实现tooltip功能。

1. 在Xaml中增加Popup控件以及进行数据绑定

        <Popup x:Name="toolTip" IsOpen="{Binding ShowToolTip, Mode=OneWay}" AllowsTransparency="True" 
            Placement="Relative" HorizontalOffset="{Binding ToolTipPos.X}" VerticalOffset="{Binding ToolTipPos.Y}">
            <TextBlock Foreground="Red" Text="{Binding DragElement.Description}"></TextBlock>
        </Popup>


注:在Popup的属性设置中特别注意Placement的属性值,详细信息可以查看MSDN  怎样设置Placement属性

2. 在ViewModel中要注意属性值的计算

        private Point _tooltipPos;
        public Point ToolTipPos
        {
            get { return _tooltipPos; }
            set
            {
                if (_tooltipPos == value)
                    return;
                if (Math.Abs(_tooltipPos.X - 15 - value.X) > 15 || Math.Abs(_tooltipPos.Y - 15 - value.Y) > 15)
                {
                    // why reduce 15? because if position the popup window to current cursor location, 
                    //if mouse moving when drag/drop, the mouse will be on popup window, so the user's experience is not good.
                    _tooltipPos = value;
                    _tooltipPos.Offset(15, 15);
                    RaisePropertyChanged("ToolTipPos");
                }
            }
        }


注意ToolTipPos的Set值的设置,实际上value是光标当前的位置,为什么要对改值Offset(15,15)?主要是为了在Popup窗口显示的时候,要保证光标所Over的控件是原来的控件,如果不进行offset,在某些细微移动的过程中可能出现DragOver的对象由原来的控件跟popup窗口之间切换。

        private bool _showToolTip;
        public bool ShowToolTip
        {
            get { return _showToolTip; }
            set
            {
                if(_showToolTip == value)
                    return;
                _showToolTip = value;
                RaisePropertyChanged("ShowToolTip");
            }
        }



该属性决定了Popup窗口是否要显示,需要在DragOver事件处理函数中进行设置为True. 在dragover对象的mousemove处理函数中设置该值为false,保证Popup消失。


3. DragOver的过程中获取鼠标的位置,设置Popup窗口的target属性。

            this.toolTip.PlacementTarget = this.SelectedEmployees;
            this.ToolTipPos = e.GetPosition(SelectedEmployees);
            ShowToolTip = true;
注: e 为dragover过程中参数DragEventArgs e,  在获取鼠标位置的时候不要用Mouse.GetPosition(IInputElement ),因为在drag过程中对鼠标是在DragOver事件中管理的, 用Mouse.GetPosition(IInputElement)不能获得准确的值。

要实现一个 **Win11 风格的 `TabControl`**,具有以下功能: - 标签页(TabItem)带关闭按钮(`×`) - 支持拖动排序 - 标签切换时有动画效果 - 美观的 Win11 主题风格(圆角、阴影、透明背景等) - 完全基于 **MVVM 模式** --- ## ✅ 实现结构 我们将使用以下组件: | 文件名 | 作用 | |--------|------| | `MainWindow.xaml` | 主窗口 | | `MainWindow.xaml.cs` | 后台代码 | | `MainViewModel.cs` | MVVM 核心逻辑 | | `TabItemViewModel.cs` | 单个标签页的数据模型 | | `RelayCommand.cs` | 命令支持 | | `DraggableTabPanel.cs` | 自定义面板,支持拖动排序 | | `TabHelper.cs` | 附加属性,绑定关闭按钮命令 | | `Styles.xaml`(可选) | Win11 风格样式资源 | --- ## ✅ 1. `MainWindow.xaml` ```xml <Window x:Class="Win11TabControl.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Win11TabControl" Title="Win11 Style TabControl" Height="600" Width="1000" Background="#F3F3F5" WindowStartupLocation="CenterScreen" BorderBrush="#CCCCCC" BorderThickness="1" CornerRadius="12" AllowsTransparency="True" WindowStyle="None" ResizeMode="CanResizeWithGrip"> <Window.DataContext> <local:MainViewModel /> </Window.DataContext> <Window.Resources> <!-- Win11风格TabItem样式 --> <Style TargetType="TabItem" x:Key="Win11TabItemStyle"> <Setter Property="Template"> <Setter.Value> <ControlTemplate> <Border BorderBrush="Transparent" BorderThickness="0" CornerRadius="8" Background="{TemplateBinding Background}" Padding="4,0"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <ContentPresenter Grid.Column="0" ContentSource="TemplatedParent" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="12,0" /> <Button Grid.Column="1" Content="×" FontSize="12" FontWeight="Bold" Width="20" Height="20" Margin="5" Background="Transparent" BorderBrush="Transparent" HorizontalAlignment="Right" VerticalAlignment="Center" ToolTip="Close Tab" local:TabHelper.CloseCommand="{Binding DataContext.CloseTabCommand, RelativeSource={RelativeSource AncestorType=Window}}"/> </Grid> </Border> <ControlTemplate.Triggers> <Trigger Property="IsSelected" Value="True"> <Setter Property="Background" Value="White"/> <Setter Property="BorderBrush" Value="#DDD"/> <Setter Property="Panel.ZIndex" Value="100"/> </Trigger> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="#E8E8EA"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> <Setter Property="Background" Value="#E8E8EA"/> <Setter Property="Margin" Value="0,0,4,0"/> <Setter Property="Padding" Value="0"/> </Style> <!-- 拖拽面板 --> <ItemsPanelTemplate x:Key="DraggableTabItemsPanel"> <local:DraggableTabPanel /> </ItemsPanelTemplate> </Window.Resources> <Grid> <TabControl x:Name="Win11TabControl" ItemsSource="{Binding Tabs}" SelectedItem="{Binding SelectedTab}" ItemContainerStyle="{StaticResource Win11TabItemStyle}" ItemsPanel="{StaticResource DraggableTabItemsPanel}" Background="White" BorderBrush="#DDD" BorderThickness="1" Margin="20" CornerRadius="8" Padding="0"/> <Button Content="Add Tab" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,25,30,0" Width="80" Click="AddTab_Click"/> </Grid> </Window> ``` --- ## ✅ 2. `MainWindow.xaml.cs` ```csharp using System.Windows; namespace Win11TabControl { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void AddTab_Click(object sender, RoutedEventArgs e) { if (DataContext is MainViewModel viewModel) { viewModel.AddTab("New Tab"); } } } } ``` --- ## ✅ 3. `MainViewModel.cs` ```csharp using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows.Input; namespace Win11TabControl { public class MainViewModel : INotifyPropertyChanged { public ObservableCollection<TabItemViewModel> Tabs { get; set; } private TabItemViewModel _selectedTab; public TabItemViewModel SelectedTab { get => _selectedTab; set { _selectedTab = value; OnPropertyChanged(); } } public ICommand CloseTabCommand { get; } public MainViewModel() { Tabs = new ObservableCollection<TabItemViewModel>(); CloseTabCommand = new RelayCommand(OnCloseTab); AddTab("Home"); AddTab("Documents"); } public void AddTab(string header) { var tab = new TabItemViewModel { Header = header, Content = $"Content of {header}" }; Tabs.Add(tab); SelectedTab = tab; } private void OnCloseTab(object param) { if (param is TabItemViewModel tab) { Tabs.Remove(tab); } } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } } ``` --- ## ✅ 4. `TabItemViewModel.cs` ```csharp using System.ComponentModel; using System.Runtime.CompilerServices; namespace Win11TabControl { public class TabItemViewModel : INotifyPropertyChanged { private string _header; private string _content; public string Header { get => _header; set { _header = value; OnPropertyChanged(); } } public string Content { get => _content; set { _content = value; OnPropertyChanged(); } } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } } ``` --- ## ✅ 5. `RelayCommand.cs` ```csharp using System; using System.Windows.Input; namespace Win11TabControl { public class RelayCommand : ICommand { private readonly Action<object> _execute; private readonly Predicate<object> _canExecute; public RelayCommand(Action<object> execute, Predicate<object> canExecute = null) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute; } public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter); public void Execute(object parameter) => _execute(parameter); public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } } } ``` --- ## ✅ 6. `DraggableTabPanel.cs` ```csharp using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace Win11TabControl { public class DraggableTabPanel : Panel { private UIElement dragElement; private Point dragStartPoint; protected override Size MeasureOverride(Size availableSize) { foreach (UIElement child in Children) { child.Measure(availableSize); } return availableSize; } protected override Size ArrangeOverride(Size finalSize) { double x = 0; foreach (UIElement child in Children) { child.Arrange(new Rect(x, 0, child.DesiredSize.Width, finalSize.Height)); x += child.DesiredSize.Width; } return finalSize; } protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) { base.OnMouseLeftButtonDown(e); var element = e.Source as UIElement; if (element != null && element.Parent == this) { dragElement = element; dragStartPoint = e.GetPosition(this); CaptureMouse(); } } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if (dragElement != null && e.LeftButton == MouseButtonState.Pressed) { Point currentPoint = e.GetPosition(this); double delta = currentPoint.X - dragStartPoint.X; int currentIndex = Children.IndexOf(dragElement); int targetIndex = currentIndex + (delta > 20 ? 1 : delta < -20 ? -1 : 0); if (targetIndex >= 0 && targetIndex < Children.Count && targetIndex != currentIndex) { AnimateSwap(currentIndex, targetIndex); Children.Remove(dragElement); Children.Insert(targetIndex, dragElement); dragStartPoint = currentPoint; } } } private void AnimateSwap(int fromIndex, int toIndex) { var from = Children[fromIndex]; var to = Children[toIndex]; var fromTransform = from.RenderTransform as TranslateTransform ?? new TranslateTransform(); var toTransform = to.RenderTransform as TranslateTransform ?? new TranslateTransform(); double fromDelta = toIndex > fromIndex ? -from.DesiredSize.Width : from.DesiredSize.Width; double toDelta = toIndex > fromIndex ? to.DesiredSize.Width : -to.DesiredSize.Width; var animation = new DoubleAnimation(fromTransform.X, fromDelta, TimeSpan.FromMilliseconds(200)); fromTransform.BeginAnimation(TranslateTransform.XProperty, animation); animation = new DoubleAnimation(toTransform.X, toDelta, TimeSpan.FromMilliseconds(200)); toTransform.BeginAnimation(TranslateTransform.XProperty, animation); from.RenderTransform = fromTransform; to.RenderTransform = toTransform; } protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) { base.OnMouseLeftButtonUp(e); dragElement = null; ReleaseMouseCapture(); } } } ``` --- ## ✅ 7. `TabHelper.cs` ```csharp using System.Windows; using System.Windows.Controls; namespace Win11TabControl { public class TabHelper { public static readonly DependencyProperty CloseCommandProperty = DependencyProperty.RegisterAttached("CloseCommand", typeof(ICommand), typeof(TabHelper), new PropertyMetadata(null, OnCloseCommandChanged)); public static ICommand GetCloseCommand(DependencyObject obj) => (ICommand)obj.GetValue(CloseCommandProperty); public static void SetCloseCommand(DependencyObject obj, ICommand value) => obj.SetValue(CloseCommandProperty, value); private static void OnCloseCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is Button button) { button.Click += (s, args) => { var command = GetCloseCommand(button); if (command != null && command.CanExecute(button.DataContext)) { command.Execute(button.DataContext); } }; } } } } ``` --- ## 🎨 Win11 风格说明 | 特性 | 描述 | |------|------| | 背景色 | 浅灰 (`#F3F3F5`) | | 标签页圆角 | `CornerRadius="8"` | | 选中背景 | 白色 | | 未选中背景 | 浅蓝灰 (`#E8E8EA`) | | 鼠标悬停效果 | 背景颜色变深 | | 关闭按钮 | 透明背景,居中显示 `×` | | 动画效果 | 拖动时使用 `TranslateTransform` 实现平滑动画 | --- ## ✅ 最终效果 - 每个标签页有关闭按钮 ✅ - 支持拖动排序 ✅ - 切换时有动画 ✅ - Win11 风格 ✅ - MVVM 架构 ✅ --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值