- UI组件
- 桌面应用
【免费下载链接】SukiUI
UI Theme for AvaloniaUI
SukiUI(AvaloniaUI 的 UI 主题框架)内置了一套完整的 Toast 通知体系:以SukiToastHost作为应用任意层级的通知宿主,以ISukiToastManager作为队列与生命周期中枢,并通过FluentSukiToastBuilder提供链式 API 快速构造通知。本篇以官方文档 toast.md 为主线,结合仓库源码深入讲解从 MVVM 接入、Toast 显示/关闭、交互回调到 Loading 与"Update"式复杂通知的完整实战方案,读完后你将能够在自己的 SukiUI 应用中独立接入并定制任意粒度的 Toast 通知。
SukiToastHost:把通知宿主挂到窗口上
SukiUI 通过SukiWindow.Hosts属性提供一种"永远渲染在最上层(包括标题栏之上)"的宿主机制,除了 SukiDialogHost 之外,默认就内置了 Toast 专用的 SukiToastHost。关于 Hosts 的整体说明可参考 hosts 文档,其中有一条重要提示:suki:SukiWindow.Hosts只在SukiWindow中有效,切勿声明在普通页面(View)里,否则不会生效。
官方推荐的标准做法是把它放进SukiWindow.Hosts,这样能获得最佳展示体验;但 Toast 宿主也可以按需本地化到应用内的任意上下文。宿主的设计是 MVVM 友好的——只要你能拿到某个SukiToastHost所使用的ISukiToastManager实例,就可以在该宿主中展示 Toast。
MVVM 方式(推荐)
<!-- XMLNS definitions omitted for brevity --> <suki:SukiWindow> <suki:SukiWindow.Hosts> <suki:SukiToastHost Manager="{Binding ToastManager}"/> </suki:SukiWindow.Hosts> </suki:SukiWindow>public class ExampleViewModel { public ISukiToastManager ToastManager { get; } = new(); }这里Manager是 SukiToastHost 上注册的StyledProperty<ISukiToastManager>。宿主持有该属性后,会在挂载到逻辑树时(OnAttachedToLogicalTree)订阅 Manager 的OnToastQueued/OnToastDismissed/OnAllToastsDismissed三个事件,从而把 Manager 的队列状态实时映射为界面上的 Toast 卡片。Demo 主窗口 SukiUIDemoView.axaml 正是这样同时声明了 Toast 与 Dialog 两个宿主:
<suki:SukiWindow.Hosts> <suki:SukiToastHost Manager="{Binding ToastManager}" /> <suki:SukiDialogHost Manager="{Binding DialogManager}" /> </suki:SukiWindow.Hosts>非 MVVM 方式(Code-Behind)
如果不希望引入 MVVM,也可以采用"开箱即用"的简单做法:在 XAML 里给宿主一个Name,然后在 Code-Behind 中把 Manager 赋给它。
<!-- XMLNS definitions omitted for brevity --> <suki:SukiWindow> <suki:SukiWindow.Hosts> <suki:SukiToastHost Name="ToastManager"/> </suki:SukiWindow.Hosts> </suki:SukiWindow>public class MainWindow : SukiWindow { public static ISukiToastManager ToastManager = new SukiToastManager(); public MainWindow() { InitializeComponent(); ToastHost.Manager = ToastManager; } }之后即可随时随地通过静态字段展示通知:
MainWindow.ToastManager.CreateToast() .Queue();注意:示例中 XAML 的Name="ToastManager"会生成名为ToastManager的字段,而构造函数里赋值的对象是ToastHost.Manager(ToastHost 是该宿主控件的字段名),两者通过Manager属性建立关联。Demo 中的 ToastWindowDemo.axaml 与 ToastWindowDemo.axaml.cs 采用的正是这种 Code-Behind 赋 Manager 的模式。
容量与位置:MaxToasts 与 Position
SukiToastHost提供两个关键可配置属性(见 SukiToastHost.cs):
MaxToasts(byte,默认5):限制单个宿主同时展示的 Toast 数量。当队列中的 Toast 超过该值时,宿主在ManagerOnToastQueued中会调用Manager.EnsureMaximum(MaxToasts)自动关闭最旧的 Toast 腾出空间(源码实现见 SukiToastManager.EnsureMaximum)。若设为<= 0,则宿主直接忽略新入队的 Toast(见 SukiToastHost.cs)。Position(ToastLocation,默认BottomRight):控制 Toast 堆叠在窗口的哪个角落。可选值定义在 ToastLocation.cs:BottomRight、BottomLeft、TopRight、TopLeft。宿主会据此设置自身的HorizontalAlignment/VerticalAlignment(见 SukiToastHost.cs)。
ISukiToastManager:Toast 队列与生命周期中枢
ISukiToastManager(接口定义)是 Toast 系统的核心抽象,宿主持有它,业务代码也通过它来入队和关闭 Toast。接口提供的能力如下:
| 成员 | 说明 |
|---|---|
event OnToastQueued | 每当一个 Toast 入队时触发(携带SukiToastQueuedEventArgs) |
event OnToastDismissed | 每当一个 Toast 被关闭时触发(携带SukiToastDismissedEventArgs,内含Toast与DismissSource) |
event OnAllToastsDismissed | 一次性关闭全部 Toast 时触发 |
Queue(ISukiToast) | 将 Toast 加入队列等待展示 |
Dismiss(toast)/Dismiss(index) | 按对象或按索引关闭指定 Toast |
DismissRange(startIndex, count) | 关闭一段范围内的 Toast |
EnsureMaximum(maxAllowed) | 保证队列不超过最大值,超出的部分按最旧优先关闭 |
DismissAll() | 立即清空全部 Toast |
IsDismissed(toast) | 判断某个 Toast 是否已被关闭 |
SetDismissTimerPollingInterval(...) | 调整关闭计时器的轮询间隔(毫秒或TimeSpan) |
默认实现 SukiToastManager 内部维护一个List<ISukiToast>队列,并用一个DispatcherTimer(默认50ms轮询一次)驱动"按时间自动关闭"的逻辑:在DismissPollingTimerOnTick中计算每个 Toast 的剩余存活时间,超时则以SukiToastDismissSource.Timeout关闭;未超时则持续更新DismissProgressValue(0~1),供界面上的倒计时进度条使用(见 SukiToastManager.cs)。值得注意的实现细节是:源码注释明确强调"事件必须在将 Toast 移出 Manager 之前触发,以保证动画只播放一次"——这解释了Dismiss方法中先触发OnToastDismissed与toast.OnDismissed、再执行RemoveAt的顺序。
关闭 Toast 的来源由枚举 SukiToastDismissSource.cs 描述:Code(代码关闭)、Click(点击关闭)、ActionButton(点击操作按钮关闭)、Timeout(超时关闭)。
用流畅构建器构造并显示 Toast
SukiUI 为 Toast 提供了流畅式(Fluent)构建器,全部扩展方法定义在 FluentSukiToastBuilder.cs 中。推荐的起点是调用ISukiToastManager上的扩展方法CreateToast(),它会返回一个SukiToastBuilder(见 FluentSukiToastBuilder.cs):
public static SukiToastBuilder CreateToast(this ISukiToastManager manager) => new(manager);之后可以任意链式调用以下方法,大部分方法都有配套的 XMLDoc 说明:
| 方法 | 作用 | 备注 |
|---|---|---|
WithTitle(string) | 设置标题 | |
WithContent(object?) | 设置正文内容 | 可以传入 ViewModel,SukiUI 会通过默认的 View 定位策略自动找到对应 View(见 FluentSukiToastBuilder.cs);也可以直接传入控件,例如ProgressBar |
OfType(NotificationType) | 设置通知类型(决定图标与颜色) | 默认Information |
WithLoadingState(bool) | 切换为 Loading 状态 | |
Dismiss().After(...)/Dismiss().ByClicking() | 配置关闭机制 | 见下一节 |
OnClicked(...)/OnDismissed(...) | 注册交互回调 | |
WithActionButton(...) | 添加操作按钮 | 可添加任意数量 |
最后调用.Queue()将 Toast 立即入队展示(SukiToastBuilder.Queue 内部调用Manager.Queue(Toast)并返回ISukiToast实例——返回实例是为了方便后续手动关闭它,例如复杂交互示例中的toastManager.Dismiss(toast))。
继续沿用上文 ViewModel 的简单示例:
public void DisplayToast() { ToastManager.CreateToast() .WithTitle("Example Toast") .WithContent("The content of an example toast can be seen here.") .Queue(); }此外还有一个便捷入口CreateSimpleInfoToast()(见 FluentSukiToastBuilder.cs):它直接返回一个"信息类型、3 秒后自动关闭、可点击关闭"的 Toast 构建器,适合不需要定制关闭逻辑的快捷场景。Demo 中 ToastsViewModel.cs 的ShowInfoToast与ShowThreeInfoToasts(连发三条)都用到了它。
关闭机制:默认不关闭,显式开启
默认情况下,Toast没有任何关闭机制——除非宿主容量(MaxToasts)被超出,此时最旧的 Toast 会被挤掉。要让 Toast 可被关闭,必须使用.Dismiss()方法开启"关闭语句",随后跟随具体的关闭方式;一个 Toast 可以同时配置多种关闭方式。
public void DisplayToast() { ToastManager.CreateToast() .Dismiss().After(TimeSpan.FromSeconds(3)) .Dismiss().ByClicking() .Queue(); }上面的例子创建了一个空 Toast:3 秒后自动关闭,或者被点击时立即关闭。
关闭方式的底层实现
.Dismiss().After(TimeSpan):调用SetDismissAfter(delay, interruptWhileHover = true)(见 SukiToastBuilder.cs)。当delay.TotalMilliseconds > 0时置CanDismissByTime = true并设置DismissTimeout;第二个参数interruptWhileHover默认为true,表示鼠标悬停在 Toast 上时暂停倒计时——这在 SukiToast.axaml.cs 的OnPointerEntered/OnPointerExited中实现:进入时把DismissStartTimestamp清零、进度重置为 1,移出后重新计时。.Dismiss().ByClicking():置CanDismissByClicking = true。当用户点击 Toast 卡片时,ToastCardClickedHandler会先触发OnClicked回调,再以SukiToastDismissSource.Click关闭(见 SukiToast.axaml.cs)。- 视觉反馈:当
CanDismissByTime为真时,Toast 顶部会显示一条倒计时进度条PART_DismissProgressBar,其Value双向绑定到DismissProgressValue(见 SukiToast.axaml)。
交互:点击回调、关闭回调与操作按钮
SukiUI 提供一对基础回调用于用户交互:.OnClicked()与.OnDismissed()。在此基础上,.WithActionButton()可以创建更复杂的交互。
public void DisplayToast() { ToastManager.CreateToast() .Dismiss().After(TimeSpan.FromSeconds(3)) .OnClicked(_ => Console.WriteLine("Toast Clicked!")) .OnDismissed(_ => Console.WriteLine("Toast Was Dismissed!")) .WithActionButton("Dismiss", _ => { }, true) .Queue(); }这段代码演示了:Toast 3 秒后自动关闭;Toast 主体可以被点击任意次数(每次触发OnClicked);而操作按钮一旦被点击,会立即关闭整个 Toast(第三个参数dismissOnClick: true),同时触发OnDismissed。
各回调的语义(见 FluentSukiToastBuilder.cs):
.OnClicked(Action<ISukiToast>):点击 Toast 卡片主体(非按钮区域)时调用。.OnDismissed(Action<ISukiToast, SukiToastDismissSource>):无论因何种原因(超时、点击、按钮、代码)被关闭时都会调用,参数中携带关闭来源,可用于区分路径。.WithActionButton(object buttonContent, Action<ISukiToast> onClicked, bool dismissOnClick = false, SukiButtonStyles style = SukiButtonStyles.Flat):添加一个操作按钮。buttonContent可以是文本,也可以是任意控件(如 ToastsViewModel.cs 中直接传入MaterialIcon图标);dismissOnClick控制点击按钮后是否同时关闭 Toast;style参数控制按钮外观,取值来自 SukiButtonStyles.cs(Basic、Flat、Accent、Icon、Danger等,支持|组合)。按钮点击时先执行回调,再按dismissOnClick决定是否以SukiToastDismissSource.ActionButton关闭(见 SukiToast.axaml.cs)。注意:AddActionButton的旧重载(bool flatStyle)已被标记[Obsolete],建议直接使用新重载并传SukiButtonStyles(见 SukiToastBuilder.cs)。
按钮在 Toast 模板中通过ItemsControl横向排列在底部区域(见 SukiToast.axaml),点击事件由SukiToast在加载时统一挂接。
Toast 类型:四种 NotificationType
调用.OfType(NotificationType.xxx)可以一键切换 Toast 的类型外观。默认类型是Information。NotificationType来自 Avalonia 的Avalonia.Controls.Notifications命名空间,SukiUI 在 SukiToastBuilder.SetType 中为每种类型映射了图标与前景色:
| 类型 | 图标(Icons) | 前景色(NotificationColor) | 代码 |
|---|---|---|---|
| Information | InformationOutline | InfoIconForeground | NotificationType.Information |
| Success | Check | SuccessIconForeground | NotificationType.Success |
| Warning | AlertOutline | WarningIconForeground | NotificationType.Warning |
| Error | AlertOutline | ErrorIconForeground | NotificationType.Error |
图标会显示在 Toast 卡片左侧的圆形徽章中,前景色决定徽章的底色;颜色资源定义在 NotificationColor.cs,图标定义在 Icons.cs。
四种类型的用法完全一致:
// Information ToastManager.CreateToast() .OfType(NotificationType.Information) .Queue(); // Success ToastManager.CreateToast() .OfType(NotificationType.Success) .Queue(); // Warning ToastManager.CreateToast() .OfType(NotificationType.Warning) .Queue(); // Error ToastManager.CreateToast() .OfType(NotificationType.Error) .Queue();Demo 中 ToastsViewModel.cs 的ShowTypeDemoToast给出了更完整的组合示例——同时设置了标题、正文、类型、3 秒自动关闭与点击关闭。
Loading Toast:显示加载状态
通过.WithLoadingState(true)可以把 Toast 切换到 Loading 状态:左侧的图标徽章会替换为一个波浪进度动画(WaveProgress,见 SukiToast.axaml):
public void DisplayToast() { ToastManager.CreateToast() .WithLoadingState(true) .Queue(); }实践建议:Loading Toast 通常配合.Dismiss().After(...)使用,否则它会一直停留在界面上直到被手动关闭。Demo 中的 ShowLoadingToast 就同时设置了 Loading 状态、3 秒自动关闭和点击关闭。
复杂交互实战:一个完整的"Update"通知
文档最后给出了一个极具代表性的场景——"版本更新"通知:先展示一个带两个操作按钮的提示,点击"Update"后切换为带进度条的更新中 Toast,进度走完后自动关闭。下面两段代码直接取自 Demo 的 ToastsViewModel.cs(与文档示例等价):
private void ShowActionToast() { toastManager.CreateToast() .WithTitle("Update Available") .WithContent("Information, Update v1.0.0.0 is Now Available.") .WithActionButtonNormal("Later", _ => { }, true) .WithActionButton("Update", _ => ShowUpdatingToast(), true) .Queue(); } private void ShowUpdatingToast() { var progress = new ProgressBar() { Value = 0, ShowProgressText = true }; var toast = toastManager.CreateToast() .WithTitle("Updating...") .WithContent(progress) .Queue(); var timer = new Timer(20); timer.Elapsed += (_, _) => { Dispatcher.UIThread.Invoke(() => { progress.Value += 1; if (progress.Value < 100) return; timer.Dispose(); toastManager.Dismiss(toast); }); }; timer.Start(); }这个例子的技术要点:
WithActionButtonNormal/WithActionButton:分别以"Basic"和"Flat"两种按钮风格添加"Later"与"Update"按钮,dismissOnClick: true使点击任一会关闭当前 Toast。Demo 中还展示了自定义按钮样式(SukiButtonStyles.Flat | SukiButtonStyles.Accent | SukiButtonStyles.Icon)与图标按钮的写法(见 ToastsViewModel.cs)。- 控件作为内容:
WithContent(progress)直接把一个ProgressBar控件当作 Toast 正文,展示任意 UI 的能力。 - 手动关闭:
Queue()返回的toast实例被保存下来,进度完成后调用toastManager.Dismiss(toast)以代码方式关闭(对应SukiToastDismissSource.Code)。 - UI 线程调度:定时器的回调通过
Dispatcher.UIThread.Invoke切回 UI 线程更新进度条,避免跨线程访问控件。
进阶实践:依赖注入、多窗口与动画机制
通过 DI 注册单例 Manager
在大型应用中,建议把ISukiToastManager注册为单例并由 DI 容器分发。Demo 的做法见 App.axaml.cs:
services.AddSingleton<ISukiToastManager, SukiToastManager>(); services.AddSingleton<ISukiDialogManager, SukiDialogManager>();各页面 ViewModel 通过构造函数注入ISukiToastManager(如 ToastsViewModel.cs),这样任何页面都能向同一个宿主投递通知。
多窗口:为每个窗口指定独立宿主
Toast 系统支持"每个窗口各自管理"或"多窗口共享"。Demo 的 ToastWindowDemo.axaml.cs 演示了三种情况:为弹出窗口新建独立的SukiToastManager并赋给该窗口的ToastHost.Manager;也可以把主窗口的 Manager 传入弹出窗口,让弹出窗口往主窗口投递 Toast;注释中还提到"可以轻松共享同一个ISukiToastManager实例"来实现多窗口统一通知。
动画与对象池:Toast 背后的实现细节
从源码结构看,Toast 的展示/关闭动画由 SukiToastMotion.cs 驱动:入队时卡片以弹性弹簧(critically damped spring)从右下角滑入并放大、模糊渐入;关闭时向右滑出、淡出并折叠高度,动画参数(时长、位移、模糊半径、弹簧频率)集中配置在 SukiToastProfile.cs(提供 Normal 与 Lite 两套预设,SukiAnimationTheme可在运行时切换,切换只影响下一次展示的 Toast)。另外,ToastPool.cs 维护了一个ConcurrentBag<ISukiToast>对象池:Toast 关闭动画结束后被归还池中,下次创建时通过ResetToDefault()复用,避免频繁创建/销毁控件带来的 GC 压力——这也是SukiToastBuilder构造时调用ToastPool.Get()(见 SukiToastBuilder.cs)的原因。
小结
SukiUI 的 Toast 体系可以概括为三条主线:宿主层(SukiToastHost声明在SukiWindow.Hosts,通过Manager绑定 +MaxToasts/Position控制容量与位置)、管理层(ISukiToastManager负责入队、关闭、批量清空与超时轮询)、构建层(FluentSukiToastBuilder的链式 API 组合标题、内容、类型、关闭方式、回调与操作按钮)。无论是简单的信息提示、带自动关闭与点击关闭的轻提示,还是带进度条的复杂"Update"通知,都可以用同一套 API 在几行代码内完成。想要深入理解每个环节,可以从 FluentSukiToastBuilder.cs、SukiToastManager.cs、SukiToastHost.cs 与 SukiToast.axaml.cs 这几个核心文件开始,结合 ToastsView.axaml 及其 ViewModel 查看全部演示用例。
- UI组件
- 桌面应用
【免费下载链接】SukiUI
UI Theme for AvaloniaUI
相关推荐
ng-zorro-antd Notification 通知模板渲染实战:用 NzNotificationService.template 构建复杂交互通知
ng zorro antd Notification 通知模板渲染实战:用 NzNotificationService.template 构建复杂交互通知 在
UI组件前端AhMyth持久化技术:确保Android RAT在设备重启后继续运行的完整指南
AhMyth持久化技术:确保Android RAT在设备重启后继续运行的完整指南 AhMyth是一款强大的跨平台Android远程管理工具,其持久化技术是确保R
网络安全渗透测试ACRA通知系统完整教程:Toast、Dialog、Notification交互详解
ACRA通知系统完整教程:Toast、Dialog、Notification交互详解 Android应用崩溃报告系统ACRA提供了多种用户交互方式,帮助开发者在
移动开发异常检测
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考