掌握 Avalonia Flyout 控件:从基础使用到高级自定义定位

1. Flyout 控件概述

Flyout 是 Avalonia UI 框架中用于临时显示内容的弹出式控件。它通常附着在另一个控件(如按钮)上,当用户与该控件交互时(例如点击),Flyout 会随即显示。与需要独立窗口的对话框不同,Flyout 更轻量,并且其生命周期通常与触发它的控件绑定,非常适合用于显示额外的选项、详细信息或上下文操作,而不会打断用户的主要操作流程。
Avalonia 提供了几种类型的 Flyout,其中最常用的是 MenuFlyout,它专门用于显示菜单项列表。理解 Flyout 的工作方式对于构建现代、流畅的桌面应用程序界面至关重要。

2. Flyout 的基本用法

2.1 静态定义 Flyout

最简单的方式是在 XAML 中静态定义 Flyout 的内容。你可以将 Flyout属性附加到支持它的控件上,例如 Button。

<Button Content="点击我" HorizontalAlignment="Center">
    <Button.Flyout>
        <Flyout Placement="Bottom">
            <StackPanel>
                <TextBlock Text="这是一个简单的 Flyout。" />
                <Button Content="内部的按钮" />
            </StackPanel>
        </Flyout>
    </Button.Flyout>
</Button>

2.2 使用 AttachedFlyout 附加到其他控件

对于不支持直接 Flyout 属性的控件,可以使用 AttachedFlyout:

<Border Background="Red" PointerPressed="Border_PointerPressed">
    <FlyoutBase.AttachedFlyout>
        <Flyout>
            <TextBlock Text="红色矩形弹出层"/>
        </Flyout>
    </FlyoutBase.AttachedFlyout>
</Border>

代码后端显示 Flyout:

public void Border_PointerPressed(object sender, PointerPressedEventArgs args)
{
    var ctl = sender as Control;
    if (ctl != null)
    {
        FlyoutBase.ShowAttachedFlyout(ctl);
    }
}

2.3 使用 MenuFlyout

MenuFlyout是 Flyout的一个特化版本,用于创建菜单。它包含 MenuItem集合,并且支持分隔符和图标。

<Button Content="选项">
    <Button.Flyout>
        <MenuFlyout Placement="Bottom">
            <MenuItem Header="新建" />
            <MenuItem Header="打开" />
            <!-- 创建分隔符 -->
            <MenuItem Header="-" />
            <MenuItem Header="保存">
                <MenuItem.Icon>
                    <PathIcon Data="{StaticResource SaveIcon}" />
                </MenuItem.Icon>
            </MenuItem>
        </MenuFlyout>
    </Button.Flyout>
</Button>

Flyout 的显示模式(ShowMode)

image

<Flyout ShowMode="Transient">
    <TextBlock Text="点击外部区域立即关闭"/>
</Flyout>

4. Flyout 展示位置完全自定义指南

4.1 基础位置配置

<!-- 顶部显示 -->
<Flyout Placement="Top">
    <TextBlock Text="在按钮上方显示"/>
</Flyout>

<!-- 底部显示 -->
<Flyout Placement="Bottom">
    <TextBlock Text="在按钮下方显示"/>
</Flyout>

<!-- 左侧显示 -->
<Flyout Placement="Left">
    <TextBlock Text="在按钮左侧显示"/>
</Flyout>

<!-- 右侧显示 -->
<Flyout Placement="Right">
    <TextBlock Text="在按钮右侧显示"/>
</Flyout>

4.2 对齐方式组合

<!-- 顶部左侧对齐 -->
<Flyout Placement="TopEdgeAlignedLeft">
    <TextBlock Text="顶部左对齐"/>
</Flyout>

<!-- 底部右侧对齐 -->
<Flyout Placement="BottomEdgeAlignedRight">
    <TextBlock Text="底部右对齐"/>
</Flyout>

<!-- 居中显示 -->
<Flyout Placement="Center">
    <TextBlock Text="居中显示"/>
</Flyout>

<!-- 全屏显示 -->
<Flyout Placement="Full">
    <TextBlock Text="全屏显示"/>
</Flyout>

4.2 对齐方式组合

<!-- 顶部左侧对齐 -->
<Flyout Placement="TopEdgeAlignedLeft">
    <TextBlock Text="顶部左对齐"/>
</Flyout>

<!-- 底部右侧对齐 -->
<Flyout Placement="BottomEdgeAlignedRight">
    <TextBlock Text="底部右对齐"/>
</Flyout>

<!-- 居中显示 -->
<Flyout Placement="Center">
    <TextBlock Text="居中显示"/>
</Flyout>

<!-- 全屏显示 -->
<Flyout Placement="Full">
    <TextBlock Text="全屏显示"/>
</Flyout>

4.3 屏幕正中央显示技巧

方法一:使用 Placement="Center"(最简单)

<Button Content="居中弹出层" Width="120" Height="40">
    <Button.Flyout>
        <Flyout Placement="Center" ShowMode="Standard">
            <StackPanel Width="200" Height="150" Background="White">
                <TextBlock Text="居中显示的弹出层" 
                          HorizontalAlignment="Center" 
                          Margin="10"/>
                <Button Content="关闭" 
                        HorizontalAlignment="Center"
                        Click="CloseFlyout"/>
            </StackPanel>
        </Flyout>
    </Button.Flyout>
</Button>

方法二:自定义样式实现精确居中

<Button Content="精确居中弹出层">
    <Button.Flyout>
        <Flyout Placement="Center" ShowMode="Transient">
            <Flyout.FlyoutPresenterClasses>
                <x:String>centeredFlyout</x:String>
            </Flyout.FlyoutPresenterClasses>
            <StackPanel Width="300" Height="200" Background="LightBlue">
                <TextBlock Text="精确居中的弹出层" 
                          FontSize="16" 
                          HorizontalAlignment="Center" 
                          Margin="20"/>
            </StackPanel>
        </Flyout>
    </Button.Flyout>
</Button>

<Style Selector="FlyoutPresenter.centeredFlyout">
    <Setter Property="HorizontalAlignment" Value="Center"/>
    <Setter Property="VerticalAlignment" Value="Center"/>
    <Setter Property="Margin" Value="0"/>
    <Setter Property="Padding" Value="0"/>
</Style>

方法三:全屏居中覆盖模式

<Button Content="全屏居中模式">
    <Button.Flyout>
        <Flyout Placement="Full" ShowMode="Transient">
            <Grid Background="#80000000">
                <Border Width="400" Height="300" 
                       Background="White" 
                       CornerRadius="10"
                       HorizontalAlignment="Center"
                       VerticalAlignment="Center">
                    <StackPanel Margin="20">
                        <TextBlock Text="全屏居中对话框" 
                                  FontSize="20" 
                                  HorizontalAlignment="Center"/>
                    </StackPanel>
                </Border>
            </Grid>
        </Flyout>
    </Button.Flyout>
</Button>

4.4 高级位置控制

使用 PlacementMode 精确控制

<Flyout Placement="Right" PlacementConstraintAdjustment="Flip">
    <Flyout.PlacementMode>
        <PlacementMode>
            <PlacementMode.PlacementAnchor>TopLeft</PlacementMode.PlacementAnchor>
            <PlacementMode.PopupAnchor>BottomRight</PlacementMode.PopupAnchor>
            <PlacementMode.Offset>10,10</PlacementMode.Offset>
        </PlacementMode>
    </Flyout.PlacementMode>
    <TextBlock Text="精确位置控制"/>
</Flyout>

动态位置调整

private void AdjustFlyoutPosition(Control targetControl)
{
    var flyout = FlyoutBase.GetAttachedFlyout(targetControl);
    var screenPosition = targetControl.PointToScreen(new Point(0, 0));
    var screenSize = Screens.Primary.Bounds.Size;
    
    if (screenPosition.Y > screenSize.Height / 2)
    {
        flyout.Placement = Placement.Top; // 向上弹出
    }
    else
    {
        flyout.Placement = Placement.Bottom; // 向下弹出
    }
}

4.5 边界约束和自适应

<Flyout Placement="Bottom" 
        PlacementConstraintAdjustment="Flip|Slide|Resize">
    <TextBlock Text="自动适应边界约束"/>
</Flyout>

image

4.6 自定义偏移和间距

<Flyout Placement="Bottom"
        HorizontalOffset="20"
        VerticalOffset="10">
    <TextBlock Text="自定义偏移位置"/>
</Flyout>

5. 高级功能与动态生成

5.1 动态生成 MenuFlyout

通过数据绑定动态创建菜单项:

public class MainViewModel
{
    public ObservableCollection<MenuItemModel> MyMenuItems { get; } = new()
    {
        new MenuItemModel { Header = "复制", Command = new RelayCommand(CopyMethod) },
        new MenuItemModel { Header = "粘贴", Command = new RelayCommand(PasteMethod) }
    };
}
<Button Content="动态菜单">
    <Button.Flyout>
        <MenuFlyout ItemsSource="{Binding MyMenuItems}" Placement="RightEdgeAlignedTop">
            <MenuFlyout.ItemContainerTheme>
                <ControlTheme TargetType="MenuItem">
                    <Setter Property="Header" Value="{Binding Header}" />
                    <Setter Property="Command" Value="{Binding Command}" />
                </ControlTheme>
            </MenuFlyout.ItemContainerTheme>
        </MenuFlyout>
    </Button.Flyout>
</Button>

5.2 共享 Flyout 资源

<Window.Resources>
    <Flyout x:Key="MySharedFlyout">
        <StackPanel>
            <TextBlock Text="共享的弹出层内容"/>
            <Button Content="操作按钮"/>
        </StackPanel>
    </Flyout>
</Window.Resources>

<Button Content="按钮1" Flyout="{StaticResource MySharedFlyout}"/>
<Button Content="按钮2" Flyout="{StaticResource MySharedFlyout}"/>

6. Flyout 样式定制

6.1 自定义 FlyoutPresenter 样式

<Style Selector="FlyoutPresenter.myCustomStyle">
    <Setter Property="Background" Value="LightBlue"/>
    <Setter Property="BorderThickness" Value="2"/>
    <Setter Property="BorderBrush" Value="DarkBlue"/>
    <Setter Property="Padding" Value="20"/>
</Style>

<Flyout FlyoutPresenterClasses="myCustomStyle">
    <TextBlock Text="自定义样式的弹出层"/>
</Flyout>

6.2 针对 MenuFlyout 的样式定制

<Style Selector="MenuFlyoutPresenter.myMenuStyle">
    <Setter Property="Background" Value="#2D2D30"/>
    <Setter Property="Foreground" Value="White"/>
</Style>

<MenuFlyout FlyoutPresenterClasses="myMenuStyle">
    <MenuItem Header="菜单项1"/>
    <MenuItem Header="菜单项2"/>
</MenuFlyout>

7. 实战案例

7.1 智能定位系统

<Border Background="LightBlue" PointerPressed="ShowSmartFlyout">
    <TextBlock Text="点击显示智能弹出层"/>
    <FlyoutBase.AttachedFlyout>
        <Flyout x:Name="SmartFlyout" PlacementConstraintAdjustment="All">
            <StackPanel>
                <TextBlock x:Name="PositionInfo" Text="位置信息"/>
                <Button Content="关闭" Click="CloseFlyout"/>
            </StackPanel>
        </Flyout>
    </FlyoutBase.AttachedFlyout>
</Border>
private void ShowSmartFlyout(object sender, PointerPressedEventArgs e)
{
    var control = sender as Control;
    if (control != null)
    {
        UpdateFlyoutPosition(control);
        FlyoutBase.ShowAttachedFlyout(control);
    }
}

private void UpdateFlyoutPosition(Control targetControl)
{
    var screen = Screens.ScreenFromPoint(targetControl.PointToScreen(new Point(0, 0)));
    var screenBounds = screen.Bounds;
    var controlCenter = targetControl.PointToScreen(
        new Point(targetControl.Bounds.Width / 2, targetControl.Bounds.Height / 2));
    
    // 选择有最多空间的方向
    var distances = new Dictionary<Placement, double>
    {
        [Placement.Top] = controlCenter.Y - screenBounds.Top,
        [Placement.Bottom] = screenBounds.Bottom - controlCenter.Y,
        [Placement.Left] = controlCenter.X - screenBounds.Left,
        [Placement.Right] = screenBounds.Right - controlCenter.X
    };
    
    var bestPlacement = distances.OrderByDescending(x => x.Value).First().Key;
    SmartFlyout.Placement = bestPlacement;
}

7.2 上下文菜单实现

<Border Background="LightGray" PointerPressed="ShowContextMenu">
    <TextBlock Text="右键点击显示菜单"/>
    <FlyoutBase.AttachedFlyout>
        <MenuFlyout ShowMode="Transient">
            <MenuItem Header="复制" Command="{Binding CopyCommand}"/>
            <MenuItem Header="粘贴" Command="{Binding PasteCommand}"/>
            <MenuItem Header="-" />
            <MenuItem Header="属性" Command="{Binding PropertiesCommand}"/>
        </MenuFlyout>
    </FlyoutBase.AttachedFlyout>
</Border>
private void ShowContextMenu(object sender, PointerPressedEventArgs e)
{
    if (e.GetCurrentPoint(sender as Control).Properties.IsRightButtonPressed)
    {
        var control = sender as Control;
        FlyoutBase.ShowAttachedFlyout(control);
    }
}

8. 最佳实践与注意事项

8.1 性能优化

  • 对于复杂内容,考虑使用延迟加载
  • 避免在 Flyout 中放置过多嵌套控件
  • 重用共享的 Flyout 资源

8.2 用户体验

  • 选择合适的 ShowMode 确保符合用户预期
  • 合理设置 Placement 避免遮挡重要内容
  • 提供清晰的关闭方式

8.3 可访问性

  • 确保键盘导航正常工作
  • 为视力障碍用户提供适当的提示
  • 测试不同 DPI 设置下的显示效果

8.4 定位策略选择指南

image

9. 总结

image

posted @ 2025-11-11 16:01  Timskt  阅读(10)  评论(0)    收藏  举报