在 Windows 运行时组件中引发事件
注意
若要详细了解如何在 C++/WinRT Windows 运行时组件中引发事件,请参阅使用 C++/WinRT 创作事件。
如果你的 Windows 运行时组件在后台线程(工作线程)中引发了用户定义的委托类型的事件,并且你希望 JavaScript 能够接收该事件,则可以使用以下方法之一实现和/或引发它。
- (选项 1)通过 Windows.UI.Core.CoreDispatcher 引发该事件,以便将该事件封送到 JavaScript 线程上下文。 尽管这通常是最佳选择,但在某些情况下,它可能无法提供最快的性能。
- (选项 2)使用 Windows.Foundation.EventHandler<Object>(但会丢失事件类型信息)。 如果选项 1 不可行或者其性能不够,这将是不错的第二选项,前提是允许丢失类型信息。 如果你在创作 C# Windows 运行时组件,则 Windows.Foundation.EventHandler<Object> 类型不可用;该类型改为投影到 System.EventHandler,因此你应改用它。
- (选项 3)为组件创建自己的代理和存根。 此选项最难实现,但它会保留类型信息,并且与要求要求的方案中的选项 1 相比,它可能提供更好的性能。
如果只是在后台线程上引发事件而不使用这些选项之一,则 JavaScript 客户端将不会收到该事件。
背景
无论使用哪种语言创建组件和应用,所有Windows 运行时组件和应用都是 COM 对象。 在 Windows API 中,大多数组件都是敏捷 COM 对象,这些对象可以与后台线程和 UI 线程上的对象通信同样良好。 如果无法使 COM 对象变得敏捷,则它需要称为代理和存根的帮助程序对象才能跨 UI 线程后台线程边界与其他 COM 对象通信。 (在 COM 术语中,这称为线程单元之间的通信。
Windows API 中的大多数对象都是敏捷的,或者内置了代理和存根。 但是,无法为泛型类型(如 Windows.Foundation)创建代理和存根。TypedEventHandler<TSender,TResult> ,因为它们不是完整的类型,直到提供类型参数。 只有 JavaScript 客户端缺少代理或存根才会成为问题,但如果希望组件可从 JavaScript 以及从 C++ 或 .NET 语言使用,则必须使用以下三个选项之一。
(选项 1)通过 CoreDispatcher 引发事件
可以使用 Windows.UI.Core.CoreDispatcher 发送任何用户定义的委托类型的事件,JavaScript 将能够接收它们。 如果不确定使用哪个选项,请先尝试此选项。 如果事件触发和事件处理之间的延迟成为问题,请尝试其他选项之一。
以下示例演示如何使用 CoreDispatcher 引发强类型事件。 请注意,类型参数为 Toast,而不是 Object。
public event EventHandler<Toast> ToastCompletedEvent;
private void OnToastCompleted(Toast args)
{
var completedEvent = ToastCompletedEvent;
if (completedEvent != null)
{
completedEvent(this, args);
}
}
public void MakeToastWithDispatcher(string message)
{
Toast toast = new Toast(message);
// Assume you have a CoreDispatcher at class scope.
// Initialize it here, then use it from the background thread.
var window = Windows.UI.Core.CoreWindow.GetForCurrentThread();
m_dispatcher = window.Dispatcher;
Task.Run( () =>
{
if (ToastCompletedEvent != null)
{
m_dispatcher.RunAsync(CoreDispatcherPriority.Normal,
new DispatchedHandler(() =>
{
this.OnToastCompleted(toast);
})); // end m_dispatcher.RunAsync
}
}); // end Task.Run
}
(选项 2)使用 EventHandler<对象> 但丢失类型信息
注意
如果你在创作 C# Windows 运行时组件,则 Windows.Foundation.EventHandler<Object> 类型不可用;该类型改为投影到 System.EventHandler,因此你应改用它。
从后台线程发送事件的另一种方法是使用 Windows.Foundation.EventHandler<对象> 作为事件的类型。 Windows 提供泛型类型的这种具体实例化,并为它提供代理和存根。 缺点是事件参数和发送方的类型信息丢失。 C++和 .NET 客户端必须通过文档了解接收事件时要转换回的类型。 JavaScript 客户端不需要原始类型信息。 它们根据元数据中的名称查找 arg 属性。
此示例演示如何在 C# 中使用 Windows.Foundation.EventHandler<对象> :
public sealed Class1
{
// Declare the event
public event EventHandler<Object> ToastCompletedEvent;
// Raise the event
public async void MakeToast(string message)
{
Toast toast = new Toast(message);
// Fire the event from a background thread to allow this thread to continue
Task.Run(() =>
{
if (ToastCompletedEvent != null)
{
OnToastCompleted(toast);
}
});
}
private void OnToastCompleted(Toast args)
{
var completedEvent = ToastCompletedEvent;
if (completedEvent != null)
{
completedEvent(this, args);
}
}
}
在 JavaScript 端使用此事件,如下所示:
toastCompletedEventHandler: function (event) {
var toastType = event.toast.toastType;
document.getElementById("toasterOutput").innerHTML = "<p>Made " + toastType + " toast</p>";
}
(选项 3)创建自己的代理和存根
若要获得具有完全保留类型信息的用户定义的事件类型的潜在性能提升,必须创建自己的代理和存根对象并将其嵌入到应用包中。 通常,只有在极少数情况下,其他两个选项都不足够的情况下,才必须使用此选项。 此外,不能保证此选项比其他两个选项提供更好的性能。 实际性能取决于许多因素。 使用 Visual Studio 探查器或其他分析工具测量应用程序中的实际性能,并确定事件是否实际上是瓶颈。
本文的其余部分介绍如何使用 C# 创建基本Windows 运行时组件,然后使用C++为代理和存根创建 DLL,使 JavaScript 能够使用组件在异步操作中引发的 Windows.Foundation.TypedEventHandler<TSender、TResult> 事件。 (也可以使用 C++ 或 Visual Basic 创建组件。与创建代理和存根相关的步骤相同。本演练基于创建Windows 运行时进程内组件示例(C++/CX),有助于解释其用途。
本演练包含以下部分。
- 在这里,你将创建两个基本Windows 运行时类。 一个类公开类型 为 Windows.Foundation.TypedEventHandler<TSender、TResult> 和其他类的事件是作为 TValue 参数返回到 JavaScript 的类型。 在完成后续步骤之前,这些类无法与 JavaScript 通信。
- 此应用激活主类对象、调用方法并处理由Windows 运行时组件引发的事件。
- 这些是生成代理和存根类的工具所必需的。
- 然后使用 IDL 文件为代理和存根生成 C 源代码。
- 注册代理存根对象,以便 COM 运行时可以找到它们,并在应用项目中引用代理存根 DLL。
创建Windows 运行时组件
在 Visual Studio 的菜单栏上,选择“ 文件 > 新建项目”。 在 “新建项目 ”对话框中,展开 JavaScript > 通用 Windows ,然后选择“ 空白应用”。 将项目命名为“烤箱应用”,然后选择“ 确定 ”按钮。
将 C# Windows 运行时组件添加到解决方案:在解决方案资源管理器中,打开解决方案的快捷菜单,然后选择“添加新>项目”。 依次展开“Visual C#”>“Microsoft Store”,然后选择“Windows 运行时组件”。 将项目命名为“烤箱”,然后选择“ 确定 ”按钮。 烤箱组件将是将在后续步骤中创建的组件的根命名空间。
在解决方案资源管理器中,打开解决方案的快捷菜单,然后选择“属性”。 在“属性页”对话框中,在左窗格中选择“配置属性”,然后在对话框顶部将“配置”设置为“调试”,将“平台”设置为“x86”、“x64”或“ARM”。 选择“确定”按钮。
重要 平台 = 任何 CPU 都不起作用,因为它对稍后要添加到解决方案的本机代码 Win32 DLL 无效。
在解决方案资源管理器中,将class1.cs重命名为ToasterComponent.cs,使其与项目的名称匹配。 Visual Studio 会自动重命名文件中的类以匹配新文件名。
在.cs文件中,添加 Windows.Foundation 命名空间的 using 指令,将 TypedEventHandler 引入范围。
当需要代理和存根时,组件必须使用接口来公开其公共成员。 在ToasterComponent.cs中,定义烤箱的接口,为烤箱生成的 Toast 定义一个接口。
请注意 ,在 C# 中,可以跳过此步骤。 相反,请先创建一个类,然后打开其快捷菜单,然后选择 “重构 > 提取接口”。 在生成的代码中,手动为接口提供公共辅助功能。
public interface IToaster
{
void MakeToast(String message);
event TypedEventHandler<Toaster, Toast> ToastCompletedEvent;
}
public interface IToast
{
String ToastType { get; }
}
IToast 接口有一个字符串,可以检索该字符串来描述 Toast 的类型。 IToaster 接口具有一个生成 Toast 的方法,以及一个指示 Toast 已创建的事件。 由于此事件返回 Toast 的特定部分(即类型),因此称为类型化事件。
接下来,我们需要实现这些接口的类,并且是公共的和密封的,以便可以从稍后要编程的 JavaScript 应用访问它们。
public sealed class Toast : IToast
{
private string _toastType;
public string ToastType
{
get
{
return _toastType;
}
}
internal Toast(String toastType)
{
_toastType = toastType;
}
}
public sealed class Toaster : IToaster
{
public event TypedEventHandler<Toaster, Toast> ToastCompletedEvent;
private void OnToastCompleted(Toast args)
{
var completedEvent = ToastCompletedEvent;
if (completedEvent != null)
{
completedEvent(this, args);
}
}
public void MakeToast(string message)
{
Toast toast = new Toast(message);
// Fire the event from a thread-pool thread to enable this thread to continue
Windows.System.Threading.ThreadPool.RunAsync(
(IAsyncAction action) =>
{
if (ToastCompletedEvent != null)
{
OnToastCompleted(toast);
}
});
}
}
在前面的代码中,我们将创建 Toast,然后启动线程池工作项以触发通知。 尽管 IDE 可能建议将 await 关键字应用于异步调用,但在这种情况下不需要这样做,因为该方法不执行依赖于操作结果的任何工作。
请注意 ,上述代码中的异步调用仅使用 ThreadPool.RunAsync 来演示在后台线程上触发事件的简单方法。 可以按以下示例所示编写此特定方法,并且工作正常,因为 .NET 任务计划程序会自动封送回 UI 线程的异步/等待调用。
public async void MakeToast(string message)
{
Toast toast = new Toast(message)
await Task.Delay(new Random().Next(1000));
OnToastCompleted(toast);
}
如果现在生成项目,它应该会完全生成。
对 JavaScript 应用进行编程
现在,我们可以向 JavaScript 应用添加一个按钮,以使其使用刚刚定义的类来生成 Toast。 在执行此操作之前,必须添加对刚刚创建的“烤箱组件”项目的引用。 在解决方案资源管理器中,打开“烤箱应用”项目的快捷菜单,选择“添加>引用”,然后选择“添加新引用”按钮。 在“添加引用”对话框中的“解决方案”下的左窗格中,选择组件项目,然后在中间窗格中选择“烤箱组件”。 选择“确定”按钮。
在解决方案资源管理器中,打开“烤箱应用”项目的快捷菜单,然后选择“设置为启动项目”。
在default.js文件的末尾,添加一个命名空间以包含函数来调用组件并被它调用。 命名空间将具有两个函数,一个函数用于生成 Toast,一个用于处理 toast 完成事件。 makeToast 的实现将创建一个烤箱对象,注册事件处理程序,并生成 Toast。 到目前为止,事件处理程序没有执行太多操作,如下所示:
WinJS.Namespace.define("ToasterApplication"), {
makeToast: function () {
var toaster = new ToasterComponent.Toaster();
//toaster.addEventListener("ontoastcompletedevent", ToasterApplication.toastCompletedEventHandler);
toaster.ontoastcompletedevent = ToasterApplication.toastCompletedEventHandler;
toaster.makeToast("Peanut Butter");
},
toastCompletedEventHandler: function(event) {
// The sender of the event (the delegate's first type parameter)
// is mapped to event.target. The second argument of the delegate
// is contained in event, which means in this case event is a
// Toast class, with a toastType string.
var toastType = event.toastType;
document.getElementById('toastOutput').innerHTML = "<p>Made " + toastType + " toast</p>";
},
});
makeToast 函数必须连接到按钮。 更新default.html以包含按钮和一些空间来输出生成 toast 的结果:
<body>
<h1>Click the button to make toast</h1>
<button onclick="ToasterApplication.makeToast()">Make Toast!</button>
<div id="toasterOutput">
<p>No Toast Yet...</p>
</div>
</body>
如果未使用 TypedEventHandler,我们现在能够在本地计算机上运行应用,然后单击该按钮进行 Toast。 但在我们的应用中,什么都没发生。 为了找出原因,让我们调试触发 ToastCompletedEvent 的托管代码。 停止项目,然后在菜单栏上,选择 “调试 > 烤箱应用程序”属性。 将调试器类型更改为“仅限托管”。 再次在菜单栏上,选择 “调试 > 异常”,然后选择“ 公共语言运行时异常”。
现在运行应用,然后单击“生成 Toast”按钮。 调试器捕获无效的强制转换异常。 尽管它从消息中并不明显,但发生此异常是因为该接口缺少代理。
为组件创建代理和存根的第一步是向接口添加唯一 ID 或 GUID。 但是,要使用的 GUID 格式因是使用 C#、Visual Basic 还是其他 .NET 语言或C++编码而有所不同。
为组件的接口生成 GUID(C# 和其他 .NET 语言)
在菜单栏上,选择“工具 > 创建 GUID”。 在对话框中,选择 5。 [Guid("xxxxxxxx-xxxx...xxxx")]。 选择“新建 GUID”按钮,然后选择“复制”按钮。
返回到接口定义,然后在 IToaster 接口之前粘贴新的 GUID,如以下示例所示。 (请勿在示例中使用 GUID。每个唯一接口都应有自己的 GUID。
[Guid("FC198F74-A808-4E2A-9255-264746965B9F")]
public interface IToaster...
为 System.Runtime.InteropServices 命名空间添加 using 指令。
对 IToast 接口重复这些步骤。
为组件的接口生成 GUID(C++)
在菜单栏上,选择“工具 > 创建 GUID”。 在对话框中,选择 3。 static const 结构 GUID = {...}。 选择“新建 GUID”按钮,然后选择“复制”按钮。
将 GUID 粘贴到 IToaster 接口定义之前。 粘贴后,GUID 应类似于以下示例。 (请勿在示例中使用 GUID。每个唯一接口都应有自己的 GUID。
// {F8D30778-9EAF-409C-BCCD-C8B24442B09B}
static const GUID <<name>> = { 0xf8d30778, 0x9eaf, 0x409c, { 0xbc, 0xcd, 0xc8, 0xb2, 0x44, 0x42, 0xb0, 0x9b } };
为 Windows.Foundation.Metadata 添加 using 指令,将 GuidAttribute 引入范围。
现在,手动将 const GUID 转换为 GuidAttribute,使其格式化,如以下示例所示。 请注意,大括号将替换为括号和括号,并删除尾部分号。
// {E976784C-AADE-4EA4-A4C0-B0C2FD1307C3}
[GuidAttribute(0xe976784c, 0xaade, 0x4ea4, 0xa4, 0xc0, 0xb0, 0xc2, 0xfd, 0x13, 0x7, 0xc3)]
public interface IToaster
{...
对 IToast 接口重复这些步骤。
现在,接口具有唯一的 ID,我们可以通过将 .winmd 文件馈送到 winmdidl 命令行工具来创建 IDL 文件,然后将该 IDL 文件馈送到 MIDL 命令行工具中,为代理生成 C 源代码并生成存根。 如果创建生成后事件,Visual Studio 会为我们执行此操作,如以下步骤所示。
生成代理和存根源代码
若要添加自定义生成后事件,请在解决方案资源管理器中打开“烤箱组件”项目的快捷菜单,然后选择“属性”。 在属性页的左窗格中,选择“生成事件”,然后选择“编辑生成后”按钮。 将以下命令添加到生成后命令行。 (必须先调用批处理文件才能设置环境变量以查找 winmdidl 工具。
call "$(DevEnvDir)..\..\vc\vcvarsall.bat" $(PlatformName)
winmdidl /outdir:output "$(TargetPath)"
midl /metadata_dir "%WindowsSdkDir%References\CommonConfiguration\Neutral" /iid "$(ProjectDir)$(TargetName)_i.c" /env win32 /h "$(ProjectDir)$(TargetName).h" /winmd "Output\$(TargetName).winmd" /W1 /char signed /nologo /winrt /dlldata "$(ProjectDir)dlldata.c" /proxy "$(ProjectDir)$(TargetName)_p.c" "Output\$(TargetName).idl"
重要提示 对于 ARM 或 x64 项目配置,请将 MIDL /env 参数更改为 x64 或 arm32。
若要确保在每次更改 .winmd 文件时重新生成 IDL 文件,请将“运行生成后事件”更改为“生成后”更新项目输出。“生成事件”属性页应如下所示:
重新生成解决方案以生成和编译 IDL。
可以通过在烤箱Component 项目目录中查找“烤箱组件”、ToasterComponent_i.c、ToasterComponent_p.c 和 dlldata.c 来验证 MIDL 是否正确编译了解决方案。
将代理和存根代码编译为 DLL
有了所需的文件后,可以将其编译为生成 DLL,这是一个C++文件。 若要使这一点尽可能简单,请添加新项目以支持生成代理。 打开 ToasterApplication 解决方案的快捷菜单,然后选择“添加”>“新建项目”。 在“新建项目”对话框的左窗格中,展开 Visual C++ > Windows > 通用 Windows,然后在中间窗格中选择 DLL(UWP 应用)。 (请注意,这不是C++ Windows 运行时组件项目。将项目代理命名,然后选择“确定”按钮。 当 C# 类中的某些内容发生更改时,生成后事件将更新这些文件。
默认情况下,代理项目生成标头 .h 文件和C++ .cpp文件。 由于 DLL 是从 MIDL 生成的文件中生成的,因此不需要 .h 和 .cpp 文件。 在解决方案资源管理器中,打开它们的快捷菜单,选择“删除”,然后确认删除。
现在项目为空,可以重新添加 MIDL 生成的文件。 打开 Proxies 项目的快捷菜单,然后选择“添加”>“现有项”。在对话框中,导航到 ToasterComponent 项目目录,然后选择 ToasterComponent.h、ToasterComponent_i.c、ToasterComponent_p.c 和 dlldata.c 文件。 选择“添加”按钮。
在代理项目中,创建一个 .def 文件以定义 dlldata.c 中所述的 DLL 导出。 打开项目的快捷菜单,然后选择“添加”>“新建项”。 在对话框的左窗格中,选择“代码”,然后在中间窗格中选择“模块定义文件”。 将文件代理命名为 def,然后选择“ 添加 ”按钮。 打开此 .def 文件并将其修改为包含 dlldata.c 中定义的 EXPORTS:
EXPORTS
DllCanUnloadNow PRIVATE
DllGetClassObject PRIVATE
如果现在生成项目,它将失败。 若要正确编译此项目,必须更改项目的编译和链接方式。 在解决方案资源管理器中,打开代理项目的快捷菜单,然后选择“属性”。 按如下所示更改属性页。
在左窗格中选择“C/C++”>“预处理器”,在右窗格中选择“预处理器定义”,选择向下箭头按钮,然后选择“编辑”。 在框中添加以下定义:
WIN32;_WINDOWS
在“C/C++”>“预编译标头”下,将“预编译标头”更改为“不使用预编译标头”,然后选择“应用”按钮。
在“链接器”>“常规”下,将“忽略导入库”更改为“是”,然后选择“应用”按钮。
在“链接器”>“输入”下,选择“附加依赖项”,选择向下箭头按钮,然后选择“编辑”。 在框中添加此文本:
rpcrt4.lib;runtimeobject.lib
不要将这些库直接粘贴到列表行中。 使用“编辑”框确保 Visual Studio 中的 MSBuild 将维护正确的附加依赖项。
进行这些更改后,请在“属性页”对话框中选择“确定”按钮。
接下来,依赖于烤箱组件项目。 这可确保在代理项目生成之前生成烤箱。 这是必需的,因为烤箱项目负责生成文件以生成代理。
打开代理项目的快捷菜单,然后选择“项目依赖项”。 选中复选框以指示代理项目依赖于烤箱组件项目,以确保 Visual Studio 按正确的顺序生成它们。
通过在 Visual Studio 菜单栏上选择“生成”>“重新生成解决方案”,验证解决方案是否已正确生成。
注册代理和存根
在“烤箱应用”项目中,打开 package.appxmanifest 的快捷菜单,然后选择“ 打开方式”。 在“打开时打开”对话框中,选择 “XML 文本编辑器 ”,然后选择“ 确定 ”按钮。 我们将粘贴提供 windows.activatableClass.proxyStub 扩展注册的一些 XML,这些 XML 基于代理中的 GUID。 若要查找在 .appxmanifest 文件中使用的 GUID,请打开 ToasterComponent_i.c。 查找类似于以下示例中的条目。 另请注意 IToast、IToaster 和第三个接口的定义-一个类型化事件处理程序,其中包含两个参数:一个烤箱和 Toast。 这与在烤箱类中定义的事件匹配。 请注意,IToast 和 IToaster 的 GUID 与 C# 文件中接口上定义的 GUID 匹配。 由于类型化的事件处理程序接口是自动生成的,因此此接口的 GUID 也会自动生成。
MIDL_DEFINE_GUID(IID, IID___FITypedEventHandler_2_ToasterComponent__CToaster_ToasterComponent__CToast,0x1ecafeff,0x1ee1,0x504a,0x9a,0xf5,0xa6,0x8c,0x6f,0xb2,0xb4,0x7d);
MIDL_DEFINE_GUID(IID, IID___x_ToasterComponent_CIToast,0xF8D30778,0x9EAF,0x409C,0xBC,0xCD,0xC8,0xB2,0x44,0x42,0xB0,0x9B);
MIDL_DEFINE_GUID(IID, IID___x_ToasterComponent_CIToaster,0xE976784C,0xAADE,0x4EA4,0xA4,0xC0,0xB0,0xC2,0xFD,0x13,0x07,0xC3);
现在复制 GUID,将其粘贴到 package.appxmanifest 中,我们添加并命名扩展的节点,然后对其进行重新格式化。 清单条目类似于以下示例,但再次记得使用自己的 GUID。 请注意,XML 中的 ClassId GUID 与 ITypedEventHandler2 相同。 这是因为 GUID 是 ToasterComponent_i.c 中列出的第一个 GUID。 此处的 GUID 不区分大小写。 可以返回到接口定义并获取具有正确格式的 GuidAttribute 值,而不是手动重新格式化 IToast 和 IToaster 的 GUID。 在C++中,注释中有格式正确的 GUID。 在任何情况下,都必须手动重新格式化用于 ClassId 和事件处理程序的 GUID。
<Extensions> <!--Use your own GUIDs!!!-->
<Extension Category="windows.activatableClass.proxyStub">
<ProxyStub ClassId="1ecafeff-1ee1-504a-9af5-a68c6fb2b47d">
<Path>Proxies.dll</Path>
<Interface Name="IToast" InterfaceId="F8D30778-9EAF-409C-BCCD-C8B24442B09B"/>
<Interface Name="IToaster" InterfaceId="E976784C-AADE-4EA4-A4C0-B0C2FD1307C3"/>
<Interface Name="ITypedEventHandler_2_ToasterComponent__CToaster_ToasterComponent__CToast" InterfaceId="1ecafeff-1ee1-504a-9af5-a68c6fb2b47d"/>
</ProxyStub>
</Extension>
</Extensions>
将 Extensions XML 节点粘贴为包节点的直接子节点,例如“资源”节点的对等节点。
在继续操作之前,请务必确保:
- ProxyStub ClassId 设置为 ToasterComponent_i.c 文件中的第一个 GUID。 使用此文件中为 classId 定义的第一个 GUID。 (这可能与 ITypedEventHandler2 的 GUID 相同。
- Path 是代理二进制文件的包相对路径。 (在本演练中,proxies.dll与 ToasterApplication.winmd 位于同一文件夹中。
- GUID 的格式正确。 (这很容易出错。
- 清单中的接口 ID 与 ToasterComponent_i.c 文件中的 IID 匹配。
- 接口名称在清单中是唯一的。 由于系统不使用这些值,因此可以选择这些值。 最好选择明确匹配已定义的接口的接口名称。 对于生成的接口,名称应指示生成的接口。 可以使用 ToasterComponent_i.c 文件来帮助生成接口名称。
如果现在尝试运行解决方案,将收到一个错误,指出proxies.dll不是有效负载的一部分。 在“烤箱应用”项目中打开“引用”文件夹的快捷菜单,然后选择“添加引用”。 选中代理项目旁边的复选框。 此外,请确保还选中了“烤箱组件”旁边的复选框。 选择“确定”按钮。
项目现在应生成。 运行项目并验证是否可以生成 Toast。