Skip to content

实践范例:模态对话框

模态框就是一个你按条件返回的元素。并没有什么特殊的 「打开对话框」API —— 当 open 为 true 时,树里会包含叠在页面之上的模态 面板;为 false 时该模态元素根本不存在,页面单独渲染。

原语

关注点 API
开/关状态 UseState<bool>
条件层次 open ? Group(page, modal) : page
遮罩 带半透明填充的外层 Border
按钮 Button(label, onClick)
生产级替代 ContentDialog

状态

var (open, setOpen) = UseState(false);
var (deleted, setDeleted) = UseState(false);

两个布尔值 —— 一个用于开/关标志,一个用于记录结果。 删除分支会翻转两者;取消分支只翻转 open

页面

// The page renders normally; the modal is just another element
// returned conditionally based on `open`.
var page = VStack(12,
    TextBlock(deleted ? "Item deleted." : "1 item selected."),
    Button("Delete…", () => setOpen(true))
).Padding(20);

页面照常渲染。按钮更新 open,从而触发一次包含该模态框的 重渲染。

// Pair the dialog with a scrim (SmokeFill) so clicks outside the
// dialog don't reach the page underneath.
Element modal = Border(
    VStack(16,
        Heading("Delete this item?"),
        TextBlock("This action cannot be undone.").Opacity(0.8),
        HStack(8,
            Button("Cancel", () => setOpen(false)),
            Button("Delete", () => { setDeleted(true); setOpen(false); })
        ).HAlign(Microsoft.UI.Xaml.HorizontalAlignment.Right)
    ).Padding(20).Background(Theme.CardBackground).CornerRadius(8)
).Background(Theme.SmokeFill).Padding(40);

删除确认模态框

该面板是一个带彩色遮罩背景的 Border。两个按钮闭包捕获了同一组 setOpensetDeleted setter,因此「取消」能干净地回退,而「删除」会在 同一次渲染中提交操作并关闭模态框。

在真实应用中,请把这个 Border 换成 dialogs-and-flyouts 里的 ContentDialog,这样焦点陷阱、 按 Esc 取消以及屏幕阅读器语义都能免费获得;上面这段范例只是用显式原语 表述了同样的形态,好让组合过程可见。

提示

把模态框渲染为兄弟节点,而不是触发按钮的子节点。 把模态框放在按钮内部会让它按按钮的边界框参与布局;通过 Group(page, modal) 渲染它,模态框才能按预期铺满宿主窗口。

在真实应用里,要在模态 Border 上做焦点陷阱。 上面的范例为了可读性省略了 焦点管理 —— 生产代码应在这个面板上调用 .FocusTrap(...),让 Tab 在对话框内部循环。完整模式见 无障碍

不要把「打开」和「保存中」混为一谈。 确认并提交的模态框有 三种状态:关闭、打开、提交中。当提交是异步的时,为「进行中」这一位再加一个 UseState<bool> —— 模式与登录范例相同。

后续阅读

  • 对话框与浮出 —— 生产级的 ContentDialog / MenuFlyout 控件。
  • 无障碍 —— 模态框的焦点陷阱与 屏幕阅读器契约。
  • 命令 —— 把删除分支接到一个 可与菜单和键盘共用的 Command<T>
  • 实践范例:登录 —— 姊妹范例 —— 相同的模式,换成 进行中状态。
  • 实践范例索引 —— 回到图库。