Avalonia 12 自定义标题栏按钮无法点击的问题与解决

问题描述

将项目从 Avalonia 11 升级到 12 后,放置在自定义标题栏区域的按钮出现了严重的命中测试(Hit Testing)问题——按钮的视觉区域正常,但可点击区域极小,仅在下边缘的 1-2 像素处才能触发点击。标题栏上的系统按钮(最小化/最大化/关闭)则完全正常。

复现环境

  • Avalonia 12.0.0
  • Semi.Avalonia 12.0.0(第三方主题)
  • Windows 11
  • .NET 9

窗口配置

<Window ...
    ExtendClientAreaToDecorationsHint="True"
    ExtendClientAreaTitleBarHeightHint="36"
    WindowDecorations="Full">

排查过程

以下方案全部无效

尝试 结论
增大 Button 尺寸(20→28→36px) 无效
Background="Transparent"Background="#01000000" 无效
给内部图标加 IsHitTestVisible="False" 无效
改用 Border + PointerPressed 替代 Button 无效
换成 Theme="{DynamicResource TertiaryButton}" 无效
改回 ExtendClientAreaToDecorationsHint="False" 能点击,但失去了标题栏一体化外观
换成下拉菜单 Button.Flyout / MenuFlyout 同样无效,菜单项也难以点击

关键线索:系统原生按钮(最小化/最大化/关闭)正常,说明问题在 Avalonia 渲染层而非 Windows 窗口系统。

根因分析

Avalonia 11 vs 12 的装饰层变化

Avalonia 12 对窗口装饰系统做了重大重构:

概念 Avalonia 11 Avalonia 12
标题栏控件 TitleBar 已移除
系统按钮 CaptionButtons 已移除
叠加层 ChromeOverlayLayer 已移除
新系统 WindowDrawnDecorations

Avalonia 12 的视觉树层次结构:

Visual root
├── Underlay layer        (边框、背景、阴影)
├── TopLevel/Window       (你的客户区内容)
├── Overlay layer         (标题栏、系统按钮) ← 在客户区之上!
├── FullscreenPopover
└── Resize hit-test zones (自动)

问题就在这里:Overlay 层位于客户区之上。当 ExtendClientAreaToDecorationsHint="True" 时,虽然你的按钮在视觉上位于标题栏区域,但 Avalonia 仍然把它们当作普通客户区内容。Overlay 层拦截了这些区域的点击事件,导致按钮的命中区域极小(只有未被 Overlay 覆盖的边缝能响应)。

为什么 Avalonia 11 正常?

Avalonia 11 使用 ExtendClientAreaChromeHints(注意是 ChromeHints 不是 Chrome)来控制哪些系统装饰可见。ChromeOverlayLayer 的 z-order 和命中测试机制不同,不会拦截客户区控件的点击。

解决方案

WindowDecorationProperties.ElementRole

Avalonia 12 提供了新的附加属性来标记位于窗口装饰区域内的用户交互元素:

<Button Command="{Binding OpenSettingsCommand}"
        WindowDecorationProperties.ElementRole="User"
        Width="28"
        Height="28"
        ... />

ElementRole 的可用值:

用途
User 用户自定义交互元素——告诉 Avalonia 放行点击
TitleBar 窗口拖拽区域
CloseButton 执行关闭窗口
MaximizeButton 执行最大化/还原
MinimizeButton 执行最小化
FullScreenButton 切换全屏
DecorationsElement 装饰模板中的通用交互元素

关键的是 User——只需要这一个属性,Avalonia 就会知道:

"这个控件是我自己画的,不是系统装饰层生成的,请让点击事件到达它。"

最终工作代码

<Window xmlns="https://github.com/avaloniaui"
        xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
        x:Class="MyApp.Views.MainWindow"
        Title="MyApp"
        ExtendClientAreaToDecorationsHint="True"
        ExtendClientAreaTitleBarHeightHint="36"
        WindowDecorations="Full"
        Background="{DynamicResource SemiColorBackground0}">

    <Grid RowDefinitions="36,*,Auto">
        <!-- 自定义标题栏 -->
        <Grid Grid.Row="0" ColumnDefinitions="Auto,*"
              Background="{DynamicResource SemiColorBackground0}"
              PointerPressed="OnTitleBarPointerPressed">

            <TextBlock Grid.Column="0"
                       Text="My App Title"
                       Margin="12,0,0,0"
                       VerticalAlignment="Center"
                       Foreground="{DynamicResource SemiColorText2}"/>

            <StackPanel Grid.Column="1"
                        Orientation="Horizontal"
                        HorizontalAlignment="Right"
                        Margin="0,0,170,0">

                <!-- 关键:ElementRole="User" -->
                <Button Command="{Binding OpenSettingsCommand}"
                        WindowDecorationProperties.ElementRole="User"
                        Width="28" Height="28"
                        Padding="0"
                        Background="Transparent"
                        BorderBrush="Transparent"
                        BorderThickness="0"
                        ToolTip.Tip="设置">
                    <materialIcons:MaterialIcon Kind="CogOutline"
                                                Width="18" Height="18"
                                                IsHitTestVisible="False"
                                                Foreground="{DynamicResource SemiColorText2}"/>
                </Button>

                <Button Command="{Binding OpenBackupCommand}"
                        WindowDecorationProperties.ElementRole="User"
                        Width="28" Height="28"
                        Padding="0"
                        Background="Transparent"
                        BorderBrush="Transparent"
                        BorderThickness="0"
                        ToolTip.Tip="备份">
                    <materialIcons:MaterialIcon Kind="ArchiveOutline"
                                                Width="18" Height="18"
                                                IsHitTestVisible="False"
                                                Foreground="{DynamicResource SemiColorText2}"/>
                </Button>
            </StackPanel>
        </Grid>
        <!-- 主内容区 -->
        ...
    </Grid>
</Window>

窗口拖拽

标题栏拖拽需要单独的代码后置处理,因为 Avalonia 12 移除了 CSS -avalonia-extensions: drag-region

// MainWindow.axaml.cs
private void OnTitleBarPointerPressed(object? sender,
    Avalonia.Input.PointerPressedEventArgs e)
{
    if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
        BeginMoveDrag(e);
}

总结

Avalonia 11 → 12 升级中,自定义标题栏按钮失效的根本原因是新增的 Overlay 装饰层截断了命中测试。解决方案就是一行附加属性:

WindowDecorationProperties.ElementRole="User"

没有这个属性,无论怎么调整 Button 样式、尺寸、背景色、事件绑定都无济于事。因为这本质上是 Avalonia 的窗口层架构问题,不是控件样式问题。


本文基于 RimTransAI 项目的实际迁移经验撰写。项目从 Avalonia 11.2.1 升级到 12.0.0,使用 Semi.Avalonia 12.0.0 主题。