从SukiUI.Demo学Avalonia

EliorFoy Lv3

1. SukiUI简介

\qquadSukiUI在众多Avalonia的UI组件库中个人认为算是比较好看的,下面是一张贴图:
基本样式图
\qquad这个不论是动画还是风格都挺好的,但是这个Demo没设计中文,同时移动端的支持不够(移动端也用这个flyout菜单感觉怪怪的,组件库似乎没有对移动端组件做特别的发布),当然其它Avalonia也有同样的问题,对移动端的支持没那么充足(这一点似乎MAUI做的更好)。

2.详细分析SukiUI.Demo

2.1 App

\qquadApp是程序的入口,包括App.xaml和App.xaml.cs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    <TrayIcon.Icons>
        <TrayIcons>
            <TrayIcon Icon="/Assets/OIG.N5o-removebg-preview.png" ToolTipText="SukiUI Native Menu Demo">
                <TrayIcon.Menu>
                    <NativeMenu>
                        <NativeMenuItem Header="Native Menu Demo">
                            <NativeMenu>
                                <NativeMenuItem Header="Option 1" />
                                <NativeMenuItem Header="Option 2" />
                                <NativeMenuItemSeparator />
                                <NativeMenuItem Header="Option 3" />
                            </NativeMenu>
                        </NativeMenuItem>
                    </NativeMenu>
                </TrayIcon.Menu>
            </TrayIcon>
        </TrayIcons>
    </TrayIcon.Icons>

\qquadTrayIcon是avalonia的默认命名空间下的非可视化组件,作系统托盘图标功能 ,在操作系统的通知区域显示应用程序图标,并提供右键菜单交互。以下是效果图:
menu.gif
\qquad可以看到<TrayIcon Icon="/Assets/OIG.N5o-removebg-preview.png" ToolTipText="SukiUI Native Menu Demo">中的ToolTipText属性确定悬停显示的文本,后面是嵌套的两个<NativeMenu>.

2.1.1 资源

1
2
3
4
5
6
7
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="Styles/CompletionWindowStyles.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>

\qquadStyles/CompletionWindowStyles.axaml具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:suki="https://github.com/kikipoulet/SukiUI"
xmlns:cc="clr-namespace:AvaloniaEdit.CodeCompletion;assembly=AvaloniaEdit">
<ControlTheme x:Key="{x:Type cc:CompletionListBox}"
BasedOn="{StaticResource {x:Type ListBox}}"
TargetType="cc:CompletionListBox">
<Setter Property="Padding" Value="0" />
<Setter Property="ItemContainerTheme">
<ControlTheme BasedOn="{StaticResource {x:Type ListBoxItem}}" TargetType="ListBoxItem">
<Setter Property="TextBlock.Foreground" Value="{DynamicResource SystemControlForegroundBaseHighBrush}" />
<Setter Property="Background" Value="Red" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseHighBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource ListBoxBorderThemeThickness}" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="Padding" Value="4,1" />
<Setter Property="FontSize" Value="12" />
<Setter Property="FontWeight" Value="DemiBold" />
</ControlTheme>
</Setter>
<Setter Property="Template">
<ControlTemplate>

<Border Name="border"
Background="{DynamicResource SukiCardBackground}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="0"
ClipToBounds="{TemplateBinding ClipToBounds}"
CornerRadius="{TemplateBinding CornerRadius}">
<Border.Resources>
<suki:BiggestItemListBoxConverter x:Key="BiggestListitem" />
</Border.Resources>
<Grid>
<ListBoxItem Margin="0,0,32,0"
Content="{TemplateBinding ItemsSource,
Converter={StaticResource BiggestListitem}}"
Opacity="0" />
<ScrollViewer Name="PART_ScrollViewer"
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
<ItemsPresenter Name="PART_ItemsPresenter"
Margin="{TemplateBinding Padding}"
ItemsPanel="{TemplateBinding ItemsPanel}" />
</ScrollViewer>
</Grid>
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>

<ControlTheme x:Key="{x:Type cc:CompletionList}" TargetType="cc:CompletionList">
<Setter Property="Template">
<ControlTemplate>
<Border Margin="10"
BoxShadow="{DynamicResource SukiPopupShadow}"
ClipToBounds="True"
CornerRadius="8">
<cc:CompletionListBox Name="PART_ListBox">
<cc:CompletionListBox.ItemTemplate>
<DataTemplate x:DataType="cc:ICompletionData">
<StackPanel Margin="0" Orientation="Horizontal">
<Image Width="16"
Height="16"
Margin="0,0,2,0"
Source="{Binding Image}" />
<ContentPresenter Content="{Binding Content}" />
</StackPanel>
</DataTemplate>
</cc:CompletionListBox.ItemTemplate>
</cc:CompletionListBox>
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

\qquad这里的cc命名空间是 AvaloniaEdit 库,它是一个用于 Avalonia UI 的代码编辑器控件库,提供了代码编辑、语法高亮、代码补全等功能。(这个demo实现了输入代码渲染的功能,所以引用了这个库)。这里ControlTheme是一个与WPF中的Style不同的地方,具体参见样式(Styling) | Avalonia Docs这里就不作详细解释了,因为用不上对某些控件也不熟悉。
\qquad一些库的style引用,因为这里没有使用nuget包,所以直接项目直接引用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://SukiUI.Dock/Index.axaml" />
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml" />
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
<suki:SukiTheme Locale="en-US" ThemeColor="Blue" />
<StyleInclude Source="avares://SukiUI.Demo/Styles/ShowMeTheXamlStyles.axaml" />
<StyleInclude Source="avares://SukiUI.Demo/Styles/WrapPanelStyles.axaml" />
<StyleInclude Source="avares://SukiUI.Demo/Styles/TextStyles.axaml" />
<StyleInclude Source="avares://SukiUI.Demo/Styles/GlassCardStyles.axaml" />
<StyleInclude Source="avares://SukiUI.Demo/Styles/MaterialIconStyles.axaml" />
<avalonia:MaterialIconStyles />
</Application.Styles>

2.1.2 code-behind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using SukiUI.Controls;
using SukiUI.Demo.Common;
using SukiUI.Demo.Features.ControlsLibrary;
using SukiUI.Demo.Features.ControlsLibrary.Colors;
using SukiUI.Demo.Features.ControlsLibrary.Dialogs;
using SukiUI.Demo.Features.ControlsLibrary.DockControls;
using SukiUI.Demo.Features.ControlsLibrary.StackPage;
using SukiUI.Demo.Features.ControlsLibrary.TabControl;
using SukiUI.Demo.Features.ControlsLibrary.Toasts;
using SukiUI.Demo.Features.CustomTheme;
using SukiUI.Demo.Features.Dashboard;
using SukiUI.Demo.Features.Effects;
using SukiUI.Demo.Features.Helpers;
using SukiUI.Demo.Features.Playground;
using SukiUI.Demo.Features.Splash;
using SukiUI.Demo.Features.Theming;
using SukiUI.Demo.Services;
using SukiUI.Dialogs;
using SukiUI.Toasts;


namespace SukiUI.Demo;

public class App : Application

{
    public override void Initialize()
    {
        AvaloniaXamlLoader.Load(this);
    }

    public override void OnFrameworkInitializationCompleted()
    {
        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
        {
            var services = new ServiceCollection();
            services.AddSingleton(desktop);
            var views = ConfigureViews(services);
            var provider = ConfigureServices(services);
            DataTemplates.Add(new ViewLocator(views));
            desktop.MainWindow = views.CreateView<SukiUIDemoViewModel>(provider) as Window;
        }

        else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
        {
            var services = new ServiceCollection();
            services.AddSingleton(singleView);
            var views = ConfigureViews(services);
            var provider = ConfigureServices(services);
            DataTemplates.Add(new ViewLocator(views));



            // Ideally, we want to create a MainView that host app content

            // and use it for both IClassicDesktopStyleApplicationLifetime and ISingleViewApplicationLifetime

            singleView.MainView = new SukiMainHost()
            {
                Hosts = [
                    new SukiDialogHost
                    {
                        Manager = new SukiDialogManager()
                    }
                ],
                Content = views.CreateView<DialogViewModel>(provider)
            };
        }

        base.OnFrameworkInitializationCompleted();


    //    Shadcn.Configure(Application.Current, ThemeVariant.Dark);
    }

    private static SukiViews ConfigureViews(ServiceCollection services)

    {

        return new SukiViews()
// 这里DI不能管理view与viewmodel的对应关系,所以又创建了一个新类来管理

            // Add main view
            .AddView<SukiUIDemoView, SukiUIDemoViewModel>(services)

            // Add pages
            .AddView<SplashView, SplashViewModel>(services)
            .AddView<ThemingView, ThemingViewModel>(services)
            .AddView<PlaygroundView, PlaygroundViewModel>(services)
            .AddView<EffectsView, EffectsViewModel>(services)
            .AddView<DashboardView, DashboardViewModel>(services)
            .AddView<ButtonsView, ButtonsViewModel>(services)
            .AddView<CardsView, CardsViewModel>(services)
            .AddView<CollectionsView, CollectionsViewModel>(services)
            .AddView<ContextMenusView, ContextMenusViewModel>(services)
            .AddView<DockView, DockViewModel>(services)
            .AddView<DockMvvmView, DockMvvmViewModel>(services)
            .AddView<ExpanderView, ExpanderViewModel>(services)
            .AddView<IconsView, IconsViewModel>(services)
            .AddView<InfoBarView, InfoBarViewModel>(services)
            .AddView<MiscView, MiscViewModel>(services)
            .AddView<ProgressView, ProgressViewModel>(services)
            .AddView<PropertyGridView, PropertyGridViewModel>(services)
            .AddView<TextView, TextViewModel>(services)
            .AddView<TogglesView, TogglesViewModel>(services)
            .AddView<ToastsView, ToastsViewModel>(services)
            .AddView<TabControlView, TabControlViewModel>(services)
            .AddView<StackPageView, StackPageViewModel>(services)
            .AddView<DialogsView, DialogsViewModel>(services)
            .AddView<HelpersView, HelpersViewModel>(services)
            .AddView<ColorsView, ColorsViewModel>(services)
            .AddView<ExperimentalView, ExperimentalViewModel>(services)

            // Add docks view for DockMvvvm
            .AddView<DocumentText, DocumentTextViewModel>(services)
            .AddView<ErrorList, ErrorListViewModel>(services)
            .AddView<OutputView, OutputViewModel>(services)
            .AddView<PropertiesView, PropertiesViewModel>(services)
            .AddView<SolutionExplore, SolutionExploreViewModel>(services)

            // Add additional views
            .AddView<DialogView, DialogViewModel>(services)
            .AddView<VmDialogView, VmDialogViewModel>(services)
            .AddView<RecursiveView, RecursiveViewModel>(services)
            .AddView<CustomThemeDialogView, CustomThemeDialogViewModel>(services);
    }
// 将一些组件同一加到依赖注入中
    private static ServiceProvider ConfigureServices(ServiceCollection services)
    {
        services.AddSingleton<ClipboardService>();
        services.AddSingleton<PageNavigationService>();
        services.AddSingleton<ISukiToastManager, SukiToastManager>();
        services.AddSingleton<ISukiDialogManager, SukiDialogManager>();
        return services.BuildServiceProvider();
    }
}

桌面应用程序配置(IClassicDesktopStyleApplicationLifetime)

  • 创建服务集合并注册桌面生命周期实例
  • 配置视图和服务
  • 设置数据模板和主窗口
    单视图应用程序配置(ISingleViewApplicationLifetime)
  • 为单视图应用程序(如移动或Web平台)配置服务
  • 创建 SukiMainHost 作为主视图容器
  • 添加对话框宿主和内容视图
    ConfigureViews() 方法
  • 创建 SukiViews 实例并注册所有视图与视图模型的对应关系
  • 使用依赖注入容器管理视图的创建和生命周期
  • 包含多种视图类型:主视图、页面视图、对话框视图等
    ConfigureServices() 方法
  • 注册应用程序所需的核心服务
  • 包括剪贴板服务、页面导航服务、对话框管理器和吐司通知管理器
  • 使用单例模式确保服务在应用程序生命周期内唯一

2.2 入口页面

\qquaddesktop.MainWindow = views.CreateView<SukiUIDemoViewModel>(provider) as Window;就确定了入口的窗口是SukiUIDemoView。看一下具体的页面布局:

1
2
3
4
5
<suki:SukiWindow.TitleBarContextMenu>
        <ContextMenu>
            <MenuItem Header="Custom TitleBar Context Menu Item" />
        </ContextMenu>
</suki:SukiWindow.TitleBarContextMenu>

\qquad这段XAML代码定义了SukiUI窗口标题栏的自定义上下文菜单(右键菜单),如图:
image-20257264951987.png

1
2
3
4
    <suki:SukiWindow.Hosts>
        <suki:SukiToastHost Manager="{Binding ToastManager}" />
        <suki:SukiDialogHost Manager="{Binding DialogManager}" />
    </suki:SukiWindow.Hosts>

\qquad这里在依赖注入的时候就注册了:

1
2
3
4
5
6
7
8
9
10
// App.axaml.cs中
services.AddSingleton<ISukiToastManager, SukiToastManager>(); // 注册Toast管理器
services.AddSingleton<ISukiDialogManager, SukiDialogManager>(); // 注册Dialog管理器
// SukiUIDemoViewModel.cs中
public SukiUIDemoViewModel(IEnumerable<DemoPageBase> demoPages, PageNavigationService pageNavigationService, ISukiToastManager toastManager, ISukiDialogManager dialogManager)
    {
        ToastManager = toastManager;
        DialogManager = dialogManager;
        // ...
    }

\qquad

a1b1...akbkbk+1ak+1...b2ka2k2k=i=1k(aia2k+1ibib2k+1i)\left| \begin{matrix} a_1 & & & & & b_1 \\ & \ddots & & & \begin{array}{c} \raisebox{1pt}{.} \\ \raisebox{4pt}{.} \\ \raisebox{7pt}{.} \end{array} & \\ & & a_k & b_k & & \\ & & b_{k+1} & a_{k+1} & & \\ & \begin{array}{c} \raisebox{1pt}{.} \\ \raisebox{4pt}{.} \\ \raisebox{7pt}{.} \end{array} & & & \ddots & \\ b_{2k} & & & & & a_{2k} \end{matrix} \right|_{2k} = \prod_{i=1}^{k} \left( a_i a_{2k+1-i} - b_i b_{2k+1-i} \right)

  • 标题: 从SukiUI.Demo学Avalonia
  • 作者: EliorFoy
  • 创建于 : 2025-07-23 19:34:16
  • 更新于 : 2025-07-29 18:41:54
  • 链接: https://eliorfoy.github.io/2025/07/23/从SukiUI.Demo学Avalonia/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论