使用 C# 在 WPF 中嵌入 IronPython





5.00/5 (2投票s)
在本文中,我们将了解如何使用 C# 将 IronPython 嵌入到我们的 Windows Presentation Framework 应用程序中。
引言
在本文中,我将解释 IronPython 作为脚本引擎嵌入到 C# 中的用法。在此过程中,我将同时展示 WPF 的基础知识以及如何将 IronPython 集成到 WPF 中,以便我们可以使用 Python 代码来编辑我们的应用程序。Python 也是一种非常容易上手的语言,特别是如果您了解 C#,因为它们非常相似。
介绍 Python
Python 编程语言于 1991 年发布,由 Guido van Rossum 创建。Python 的语法非常清晰简洁,将程序员的精力放在计算机而不是计算机的精力上。该语言本身是一种多范式语言,与 Perl、Ruby 等语言相似。Python 是一个由非营利性 Python 软件基金会管理的开放社区开发模型。
C# 和 Python 的简要区别
Python 的动态类型模型允许它在运行时自动确定数据类型。无需提前声明变量的类型,这是一个非常简单的概念。
声明 C# 和 Python 中变量的区别
int a = 1
string b = "b"
a = 1
b = 'b'
在 Python 中创建 if
语句与在 C# 中几乎相同,只是 Python 不使用大括号 ({}) 来开始和结束方法。相反,在语句的开头添加冒号 (:) 来开始执行代码。一个问题出现了,因为没有像 C# 的大括号那样的结束指示,所以您只能写一个语句而没有任何其他指示。这可以通过在每个语句末尾加上分号 (;) 来解决,以表明我们还没有完成方法。
在 C# 和 Python 中创建 if
语句的区别
if (a > b)
{
a = 1;
b = 2;
}
else if (a < 3 and b > 3)
{
a = 2;
}
else
{
b = 3;
}
if (a > b):
a = 1;
b = 2;
elif (a < 3 and b > 3):
a = 2
else:
b = 3
在 Python 中声明的函数与之前的 if
语句几乎相同,只是它以 "def
" 开头。Python 的 def
是可执行代码,因此当您编译代码时,函数在 Python 达到并运行 def
之前不存在。函数类型(如变量)不需要声明类型。
在 C# 和 Python 中创建函数的区别
int MyFunction()
{
return 5;
}
def MyFunction():
return 5;
以上是对 Python 的非常简短的介绍。另外,请注意,Python 的许多语法可能与此处所示的不同,但可能表示相同的意思。
IronPython 简介
IronPython 是通过实现 Python 语言而创建的,该语言是为 .NET 环境构建的。IronPython 的创建者是 Jim Hugunnin,IronPython 的第一个版本发布于 2006 年 9 月 5 日。
嵌入 IronPython
IronPython 可以通过几个简单的步骤嵌入到 WPF (Windows Presentation Framework) 应用程序中。
- 引用 IronPython 和 IronMath。
- 添加命名空间
- 声明
PythonEngine
using IronPython;
using IronMath;
engine = new PythonEngine();
通过完成这三个步骤,您已经初始化了 PythonEngine
开始所需的所有内容。
在您的应用程序中使用 IronPython 主要在于声明变量和加载 Python 脚本(*.py 扩展名)文件。
//Add Variable
PythonEngine.Globals.Add(key, value);
//Load Python File
PythonEngine.CompileFile(string path);
将变量添加到 PythonEngine
全局变量的示例
int var = 1;
PythonEngine.Globals.Add("var", var);
PythonEngine.Globals["var"] = 3;
编译 Python (*.py) 文件的示例
//PythonFile.py
//
name = 'Chris'
age = 21
//Example.cs
PythonEngine.CompileFile("PythonFile.py");
//Retrieve Variables
string name = PythonEngine["name"].ToString();
int age = (int)PythonEngine["age"] ;
正如您所见,PythonEngine.Globals
在 C# 和 Python 通信中起着重要作用。
现在,假设您想执行一个简单的命令,例如,在 C# 中使用 Python 的 print
命令。有一个简单的函数可以做到这一点。
//Execute code
PythonEngine.Execute(print name)
//Ouputs to the stream
PythonEngine.SetStandardOutput(Stream s)
在 C# 中使用此功能的示例
//
//ExecutePython.cs
//
string name = "Bob";
MemoryStream stream = new MemoryStream();
PythonEngine.Globals.Add("name", name);
PythonEngine.SetStandardOutput(stream);
PythonEngine.Execute("print name")
//Retrieve data from stream
byte[] data = new byte[stream.Length];
stream.Seek(0, SeekOrigin.Begin);
stream.Read(data, 0, data.Length);
string strdata = Encoding.ASCII.GetString(data);
//Output
//strdata: "Bob"
您还可以连接输入和错误流。
PythonEngine.SetStandardError(Stream s);
PythonEngine.SetStandardInput(Stream s);
使用 WPF 创建应用程序
我在这里创建的应用程序是使用 Windows Presentation Framework 创建的,该程序的目标是用于试验 IronPython 并了解其优势。我将介绍用于创建应用程序基本 UI 的 XAML 和 C# 代码。
为了获得应用程序的“Aero”外观,我必须执行以下步骤。
- 添加
PresentationFramework.Aero
引用。 - 之后,右键单击引用 PresentationFramework.Aero 并选择:将本地副本设置为 true。
- 打开 App.xaml 并添加/编辑
- 单击构建应用程序。
<ResourceDictionary
Source="/presentationframework.aero;component/themes/aero.normalcolor.xaml" />
Aero 外观现在已应用于 UI。
此应用程序中使用的 TreeView
由父 Scene
节点和 Scene
节点的子节点组成。Scene
节点由三个子节点组成,分别是 Script
、Actors
和 Objects
。Script
节点有一个作为子项的组合框,以便用户可以选择当前要渲染到场景的脚本。Actors
和 Objects
留空以供任何子项使用。节点显示名称使用 Header
属性设置,并使用 IsExpanded="True"
属性展开。
<TreeView Name="treeScene" Background="LightGray" Width="135">
<TreeViewItem Header="Scene" IsExpanded="True">
<TreeViewItem Header="Script">
<ComboBox Name="comboScript" />
</TreeViewItem>
<TreeViewItem Header="Actors" IsExpanded="True" />
<TreeViewItem Header="Objects" IsExpanded="True" />
</TreeViewItem>
</TreeView>
创建 Actor、Script 或 Object 后,TreeView
会创建和/或更新新内容。使用新内容更新 TreeView
包括以下步骤:
- 创建 Actor。
- 将
TreeViewItem
添加到 Actors 节点。 - 在刚创建的 Actor 节点下创建另一个
TreeViewItem
。 - 将
ComboBox
添加到最新创建的TreeViewItem
。
这是实现此过程的 C# 代码。
public void AddTreeItem()
{
TreeViewItem treeItemActor = new TreeViewItem();
treeItemActor.IsExpanded = true;
treeItemActor.Header = actorName;
TreeViewItem treeItemScript = new TreeViewItem();
treeItemScript.IsExpanded = true;
treeItemScript.Header = "Script";
ActorScript = new ComboBox();
foreach (string scriptName in AIEngine.ScriptFiles.Keys)
{
actorScript.Items.Add(scriptName);
}
treeItemScript.Items.Add(ActorScript);
int actorIndex = ((TreeViewItem)((TreeViewItem)
AIEngine.SceneTree.Items[0]).Items[1]).Items.Add(treeItemActor);
((TreeViewItem)((TreeViewItem)((TreeViewItem)
AIEngine.SceneTree.Items[0]).Items[1]).Items[actorIndex]).Items.Add(
treeItemScript);
}
每次创建 Actor 时,此过程都会在应用程序中实现。
Screen
由自定义屏幕创建,该屏幕通过继承 DrawingCanvas
类(该类继承 Canvas
类)来创建。DrawingCanvas
类充当用户可以在其上绘制对象的控件。为此,该类必须具有 System.Windows.Media.Visuals
的集合(其中保存了绘图)。此外,我还创建了一个临时视觉集合,以便我可以在不弄乱主视觉集合的情况下制作方形绘图动画。
此处的代码显示了如何实现这一点。
//The screen that holds and draws visuals. This is embedded
//in the XAML code of the MainWnd.
public class DrawingCanvas : Canvas
{
//The collection of visual(drawings) the screen has
private List<Visual>
visuals = new List<Visual>();
//Temprorary visuals that are deleted periodicly.
//Such as drawing the square, in order to animate the
//dragging ability, we must delete visuals.
private List<Visual>
tempvisuals = new List<Visual>();
//This tells the AddVisual whether or not add the visual
//temprorary or in the visual collection.
public bool startTempVisual = false;
//Get the current visual count
protected override int VisualChildrenCount
{
get
{
if (!startTempVisual)
{
return visuals.Count;
}
else
{
return tempvisuals.Count;
}
}
}
//Get a visual
protected override Visual GetVisualChild(int index)
{
if (!startTempVisual)
{
return visuals[index];
}
else
{
return tempvisuals[index];
}
}
//Add a temporary or normal visual
public void AddVisual(Visual visual, bool tempVisual)
{
if (tempVisual)
{
tempvisuals.Add(visual);
}
else
{
visuals.Add(visual);
}
base.AddVisualChild(visual);
base.AddLogicalChild(visual);
}
//Delete a temporary or normal visual
public void DeleteVisual(Visual visual, bool tempVisual)
{
if (tempVisual)
{
tempvisuals.Clear();
}
else
{
visuals.Remove(visual);
}
base.RemoveVisualChild(visual);
base.RemoveLogicalChild(visual);
}
}
然后将 DrawingCanvas
类嵌入到 MainWindow
的 XAML 中。
<local:DrawingCanvas Name="drawingScreen" Background="DimGray"></local:DrawingCanvas>
“local:
”标签用于从命名空间引用 DrawingCanvas
。
xmlns:local="clr-namespace:AIEditor.Core;assembly=AIEditor.Core"
Screen
背景中显示的网格是使用 System.Windows.Media.DrawLine
绘制并添加到 DrawingCanvas
的视觉集合中的。
窗口加载时,将触发此事件以绘制背景网格。
private void Window_Loaded(object sender, RoutedEventArgs e)
{
visual = new DrawingVisual();
int heightIncrements = (int)drawScreen.RenderSize.Height / 10;
int widthIncrements = (int)drawScreen.RenderSize.Width / 10;
int horizontalLength = (int)drawScreen.RenderSize.Width;
int verticalLength = (int)drawScreen.RenderSize.Height;
float verticalIncrement = 0;
float horizontalIncrement = 0;
float largerIndicator = 0;
using (DrawingContext dc = visual.RenderOpen())
{
//Draw Horizontal Lines
for (int h = 0; h < heightIncrements + 1; h++)
{
if (largerIndicator == 5)
{
Point fromPoint = new Point(0, verticalIncrement);
Point toPoint = new Point(horizontalLength, verticalIncrement);
HorizontalLines.Add(new Point[] { fromPoint, toPoint });
Pen pen = new Pen(Brushes.DarkKhaki, .5);
dc.DrawLine(pen, fromPoint, toPoint);
largerIndicator = 0;
}
else
{
Point fromPoint = new Point(0, verticalIncrement);
Point toPoint = new Point(horizontalLength, verticalIncrement);
HorizontalLines.Add(new Point[] { fromPoint, toPoint });
Pen pen = new Pen(Brushes.Gray, .5);
dc.DrawLine(pen, fromPoint, toPoint);
}
largerIndicator += 1;
verticalIncrement += 10;
}
largerIndicator = 0;
//Draw Vertical Lines
for (int w = 0; w < widthIncrements + 1; w++)
{
if (largerIndicator == 5)
{
Point fromPoint = new Point(horizontalIncrement, verticalLength);
Point toPoint = new Point(horizontalIncrement, 0);
VerticalLines.Add(new Point[] { fromPoint, toPoint });
Pen pen = new Pen(Brushes.DarkKhaki, .5);
dc.DrawLine(pen, fromPoint, toPoint);
largerIndicator = 0;
}
else
{
Point fromPoint = new Point(horizontalIncrement, verticalLength);
Point toPoint = new Point(horizontalIncrement, 0);
VerticalLines.Add(new Point[] { fromPoint, toPoint });
Pen pen = new Pen(Brushes.Gray, .5);
dc.DrawLine(pen, fromPoint, toPoint);
}
largerIndicator += 1;
horizontalIncrement += 10;
}
}
drawScreen.AddVisual(visual, false);
}
此绘图首先将屏幕大小除以十以获得每条网格线之间的间隔。我们通过 for
循环遍历每个间隔,并设置绘制所需的 fromPoint
和 toPoint
绘制位置,以绘制从点 A 到点 B 的线。horizontalIncrement
和 verticalIncrement
存储当前递增位置以绘制下一条线,当我们绘制线时,我们希望线延伸到屏幕的末端,因此我们使用 horizontalLength
和 vertialLength
来绘制到点。当每增加 5 条线时,画笔颜色将更改为 Dark Khaki。在遍历完所有垂直和水平增量后,我们将最终视觉添加到 DrawingCanvas
。
在此应用程序中,创建 Actor 的过程非常直接。应用程序遵循以下步骤来创建和绘制 Actor:
- 单击“创建 Actor”菜单项。
- 单击
DrawingCanvas
屏幕。 - 调用
DrawActor
,并将一个视觉对象添加到DrawingCanvas
。
这是“创建 Actor”菜单项的 XAML。
<ToolBar Height="25"
Margin="0,18,2,0"
Name="toolBarMain" VerticalAlignment="Top"
Grid.Column="1">
<Button Name="btnCreateActor" Content="Create Actor" />
</ToolBar>
设置 Actor 工具
private void btnCreateActor_Click(object sender, EventArgs e)
{
Engine.Draw.CurrentTool = AIDraw.DrawingTools.Actor;
}
调用 DrawActor
将 Actor 添加到 DrawingCanvas
视觉对象中。
//Create an Actor
public void DrawActor(){
visual = new DrawingVisual();
using (DrawingContext dc = visual.RenderOpen())
{
dc.DrawEllipse(drawingBrush, drawingPen,
fromMousePoint, 4, 4);
}
drawScreen.AddVisual(visual, true);
}
在此过程结束时,您的 Actor 将被添加到 DrawingCanvas
的视觉对象中并添加到 Screen
。
方形绘图是两者中最复杂的,因为它具有拖动正方形边缘到任意所需大小的动画效果,同时保持正方形形状。此绘图还使用临时视觉集合来获得其动画效果。创建正方形时遵循的流程:
- 单击“创建正方形”菜单项。
- 在屏幕上单击并拖动鼠标以绘制正方形。
- 通过放开鼠标左键退出绘图过程,并将正方形视觉对象添加。
这是 XAML 中的“CreateSquare”菜单项。
<ToolBar Height="25"
Margin="0,18,2,0"
Name="toolBarMain" VerticalAlignment="Top"
Grid.Column="1">
<Button Name="btnCreateSquare" Content="Create Square" />
</ToolBar>
设置正方形工具
private void btnCreateSquare_Click(object sender, EventArgs e)<
{
Engine.Draw.CurrentTool = AIDraw.DrawingTools.Square;
}
调用 DrawSquare
。
//Create a Square
public void DrawSquare(Point ToPoint)
{
if (cleanupFirstVisual)
drawScreen.DeleteVisual(visual, true);
visual = new DrawingVisual();
using (DrawingContext dc = visual.RenderOpen())
{
Brush brush = drawingBrush;
dc.DrawRectangle(null, drawingPen, new Rect(fromMousePoint, ToPoint));
}
drawScreen.AddVisual(visual, true);
grabLastVisual = visual;
if (!cleanupFirstVisual)
cleanupFirstVisual = true;
toMousePoint = ToPoint;
}
DrawSquare
与 DrawActor
相比非常不同。这是因为现在我们必须添加临时视觉对象,以便在拖动正方形时,可以删除集合中的前一个视觉对象。如果我们不删除前一个视觉对象,每次移动鼠标时都会看到无数个正方形被绘制。
当鼠标左键按下且我们正在移动鼠标(以调整正方形大小)时,此事件会被激活。
private void drawingScreen_MouseMove(object sender, EventArgs e)
{
Point position = MousePosition;
if (drawsquare)
{
DrawSquare(MousePosition);
}
}
现在,当所有这些发生时,我们将内容添加到 DrawingCanvas
temporaryvisual
集合中并定期删除它们。但是,现在我们需要一个事件来处理鼠标左键抬起的情况,这样我们就可以将最终视觉添加到主视觉集合中,并且临时视觉集合将被完全清除。
private void drawingScreen_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
cleanupFirstVisual = false;
if (drawsquare)
{
drawsquare = false;
drawScreen.startTempVisual = false;
drawScreen.DeleteVisual(visual, true);
drawScreen.AddVisual(grabLastVisual, false);
CurrentTool = DrawingTools.Arrow;
}
}
如果我们想在拖动正方形时取消绘制怎么办?我们只需检查鼠标右键单击事件即可删除当前正在绘制的视觉对象。
private void drawingScreen_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
drawScreen.DeleteVisual(visual, true);
}
现在,这是此应用程序中一些 WPF 控件的基本概述。
使用应用程序(在 WPF 中试验 IronPython)
本文介绍的应用程序主要基于在 Windows Presentation Framework 中试验 IronPython。它设置为您可以导入 Actor Python 脚本和 Scene Python 脚本。两者之间的区别在于,Actor 脚本仅初始化一次,而 Scene 脚本在每个帧中都初始化。在游戏循环中,Scene 脚本在每个帧中渲染,这会调用 DispatcherTimer
。DispatcherTimer
允许您控制场景渲染速度的间隔,例如每秒帧数。
我还包含了一些简单的 Python 代码示例,位于 zip 文件的 PythonSamples 文件夹中。我将简要解释这些示例的用法。
绘制视觉示例
DrawingVisual 文件夹中的 DrawingVisual 示例是一种非常简单、直接的在 Python 中使用 System.Windows.Media.Visuals
和 System.Windows.Media.DrawingContexts
进行绘图的方法。
首先,让我们看看 C# 代码,看看 Python 完成绘图过程需要什么。
我们需要将 DrawingCanvas
类设置到 IronPython 的全局变量中。
DrawingCanvas drawScreen;
ScriptEngine.Globals.Add("drawScreen", drawScreen);
C# 代码就到这里,现在来看 Python 代码。
我们需要声明我们的 Actor.py 代码。此代码将只初始化一次,与 Scene.py 文件不同。
//Actor.py
visual = System.Windows.Media.DrawingVisual()
context = System.Windows.Media.DrawingContext
pen = System.Windows.Media.Pen(System.Windows.Media.Brushes.Purple, 3)
brush = System.Windows.Media.Brush
brush = System.Windows.Media.Brushes.Blue
context = visual.RenderOpen()
context.DrawRectangle(brush, pen, System.Windows.Rect(Point(5,50), Point(400,400)))
context.Close()
drawScreen.AddVisual(visual, False)
笔刷和填充是 System.Windows.Media
命名空间提供的简单属性。视觉对象和上下文是绘制对象到 DrawingCanvas
的定义部分。在声明之后,我们启动 visual.RenderOpen()
来表示我们将开始创建我们的视觉对象。然后我们使用 context.DrawRectangle
通过输入声明的属性来初始化我们的新 Rectangle
。在 DrawRectangle
声明之后,我们关闭上下文,以便我们可以停止绘制新的视觉对象。然后将视觉对象添加到我们的 drawScreen
,即屏幕的 DrawingCanvas
。我们还将 drawScreen.AddVisual
中的第二个参数设置为 false
,因为我们不希望将视觉对象绘制为临时视觉对象。
Scene.py 文件留空,因为我们不需要在游戏循环中持续渲染视觉对象才能在屏幕上看到它。
现在,要查看绘图视觉对象在实际中的效果。启动应用程序,单击“创建 Actor”工具栏项,然后选择您希望创建 Actor 的位置。然后,单击“脚本”菜单项并选择“导入”。从 PythonSamples/DrawingVisuals 文件夹中选择 Scene.py 和 Actor.py。完成此操作后,进入 TreeView,然后在 Scene 下,您应该会看到 Script 节点,在其下方应该有一个组合框。在组合框中选择 Scene.py,并对 Actor 执行相同的操作,选择 Actor.py。在 Scene 和 Actor 选择脚本后,单击工具栏中的 Play。您应该会看到一个框被绘制。
输入示例
PythonSamples 的 Input 文件夹包含一个关于在 IronPython 和 C# 之间添加键盘输入通信的示例。为了将键盘事件传递给 IronPython,我必须首先在 IronPython 的全局变量中创建一个 Key
变量。
Key input = new Key();
ScriptEngine.Globals.Add("key", input);
设置好之后,我们需要在 MainWindow
中创建一个键盘事件,以便在按下键盘时设置 ScriptEngine
中的 key 变量。
private void MainWnd_KeyDown(object sender, KeyEventArgs e)
{
//Pass the key input to the ScriptEngine so it may be used
AIEngine.ScriptEngine.Globals["key"] = e.Key;
}
此外,我们还需要跟踪何时释放键盘,以便知道何时发送 null 键。
private void MainWnd_KeyUp(object sender, KeyEventArgs e)
{
//If no keys are being hit, send a null value to the PythonEngine
AIEngine.ScriptEngine.Globals["key"] = null;
}
运行应用程序时,单击“创建 Actor”并选择一个 Actor 在屏幕上创建。
现在,由于我们所有的 C# 代码都在更新 ScriptEngine
中的 key
变量,剩下的就是 Python 代码了。
首先,让我们在 Actor.py 文件中声明我们的 Actor 的移动变量。
#Actor.py
actorPosX = 250
actorPosY = 250
这些变量将是 Actor 的默认位置。
让我们创建 Scene.py,其中将包含键盘输入检查。我们只需要一个 if
语句来检查我们想要的每个键,然后如果按下键,就递增 Actor 的位置。
#Scene.py
if (key == System.Windows.Input.Key.W):
actorPosY -= 1
if (key == System.Windows.Input.Key.S):
actorPosY += 1
if (key == System.Windows.Input.Key.D):
actorPosX += 1
if (key == System.Windows.Input.Key.A):
actorPosX -= 1
Actor9.ActorPosition = System.Windows.Point(actorPosX, actorPosY)
Actor9.ActorPosition
变量是 AIEditor.Core.AIActor
中的一个属性,用于设置 Actor 的位置并重绘视觉对象以反映新位置。如您所见,Actor9
变量并未在 AIActor
类中声明或设置。当我们在场景中创建 Actor 时,我们的 Scene TreeView
会使用新创建的 Actor 名称进行更新,该名称将类似于 Actor9、Actor14 等。所有这些 Actor 都已通过我们正常的 Actor 创建过程添加到 ScriptEngine.Globals
中。因此,您可能需要根据您的应用程序中 Actor 的名称来更新 Actor9 的名称。
您可能还想知道我们是如何在 Python 脚本中使用一些 System.Windows
命名空间的。PythonEngine 中有一个函数允许您加载程序集。
PythonEngine.LoadAssembly(Assembly assembly);
这使我们能够加载 System.Windows.Point
。
ScriptEngine.LoadAssembly(Assembly.Load("WindowsBase"));
将程序集添加到 Python 脚本中是一个巨大的好处,它将使您能够创建结构良好的 IronPython 应用程序。
还有一个命名空间导入函数。
PythonEngine.Import(string namespace)
现在回到主要话题。运行应用程序时,您应该首先从“创建 Actor”菜单项创建一个 Actor,然后通过单击“脚本”菜单项并选择“导入”,从 PythonSamples 文件夹导入 Actor.py 和 Scene.py 脚本。完成此操作后,转到您的 Actors 节点,在其下方,应该有一个标题类似于 Actor#(# 是分配给 Actor 的数字)的 TreeViewItem
。展开节点直到到达 Actor# 的 Script 的组合框,然后选择 Actor.py。完成此操作后,还必须在 Scene 父节点正下方的 Script 节点中选择 Scene.py 脚本。所有 Python 文件都选定后,单击“Play”菜单项。这将开始游戏循环并编译脚本。如果脚本中有错误,它还会弹出一个消息框。现在,按下键盘上的 W、S、A、D 键中的任何一个,您应该会看到您的 Actor 在场景中移动。
要查看绘图视觉对象在实际中的效果,请启动应用程序,单击“创建 Actor”工具栏项并选择您希望创建 Actor 的位置。然后,单击“脚本”菜单项并选择“导入”。从 PythonSamples/DrawingVisuals 文件夹中选择 Scene.py 和 Actor.py。完成此操作后,进入 TreeView,然后在 Scene 下,您应该会看到 Script 节点,在其下方应该有一个组合框。在组合框中选择 Scene.py,并对 Actor 执行相同的操作,选择 Actor.py。在 Scene 和 Actor 选择脚本后,单击工具栏中的 Play,您应该会看到一个框被绘制。