JavaFX 2.0 事件处理


1 JavaFX 事件处理 在 JavaFX 应用中,事件就是通知有什么事发生了。当用户点击按钮、敲击键盘、移动鼠标 或执行别的行为时,事件就会被派遣;而在应用内住处事件过滤器和事件处理器来接收事件, 并作出响应。这部分教程介绍如何处理事件,并提供相应事件处理的例子。内容包括:  事件处理:介绍 JavaFX 应用中如何处理事件的基础架构;  使用事件的便捷方法:介绍了最简洁的用户与应用交互事件的处理方法;  使用事件过滤器:通过示例说明事件过滤器如何用于处理事件;  使用事件处理器:通过示例说明事件处理器如何用于处理事件。 1.1 事件处理 这部分主题是描述 javaFX 应用中的事件以及事件处理。通过这部分内容来学习事件类 型、事件目标、事件捕获、事件凸显(Event Bubbling)以及事件处理系统的基础架构。 事件适用于通知应用的用户行为并使应用对事件做出响应。JavaFX 平台提供了事件捕获 的结构、事件目标路由,并根据需要使应用能处理相应事件。 1.1.1 事件(Events) 一个事件代表了对应用有意义的事情的出现,如移动鼠标、敲击键盘等。在 JavaFX 中, 一个事件是 javafx.event.Event 类的实例,或是任意的 Event 子类。JavaFX 提供了好几种事件, 包括 DragEvent、KeyEvent、MouseEvent、ScrollEvent 以及其它。也可扩展 Event 类来定义自 己的事件。 每类事件包含的信息如表 1-1 所示。 表 1-1 事件特性 特性 描述 事件类型 发生事件的类型 事件源 事件源,对事件派遣链列表中事件位置的响应。源会随事件在链表中 的传递而改变 目标 即发生行为的节点和时间链表中最终节点。目标不改变,但若在事件 捕获节点被事件过滤器销毁,则目标不会接受到事件。 每个事件类型的子类事件都提供了相应特定的附加信息。例如 MouseEvent 类包括了哪个按 钮被按的信息、按按钮的次数以及鼠标的位置等。 1.1.2 事件类型(Event Types) 事件类型是 EventType 类的实例。单个事件类型进一步可分为多个类型。如 KeyEvent 类包含 如下事件类型:  KEY_PRESSED  KEY_RELEASED  KEY_TYPED 事件类型是分层的,每个事件类型有个名字和超类。例如按键事件的名字是 KEY_PRESSED, 超类型是 KeyEvent.ANY。顶层事件的超类事件类型是 null。图 1-1 展示了事件类型的层级结 构一子集。 图 1-1 事件类型层级 在事件层级关系中顶级事件类型是 Event.ROOT,等价于 Event.ANY。在子类型中,ANY 类型 用于表示任意事件类。例如,为实现对任意键盘事件的响应,可使用 KeyEvent.ANY 类型来 作为事件过滤器或处理器;而对于只要响应释放键事件,使用 KeyEvent.KEY_RELEASED 类型 来过来或处理。 1.1.3 事件目标(Event Targets) 事件目标可以是任何实现了 EventTarget 接口类的实例。buildEventDispatchChain 的实现, 创建了事件派发链表,且事件必须依此链表到达目标。 Window、Scene 和 Node 类实现了 EventTarget 接口,且子类也继承了浙西实现。因此用 户界面中大多数元素拥有定义好的事件派发链表,以便能集中事件响应,而不需要创建事件 派发链表。 如果是自定义 UI 控件来响应用户行为,且控件使 Window、Scene 或 Node 的子类,那 么控件通过继承也是个事件目标。如果控件或控件元素不是 Window、Scene 或 Node 子类, 则必须自行实现控件或元素的 EventTarget 接口。例如,MenuBar 控件通过继承成为事件目 标,而 MenuItem 元素必须实现 EventTarget 接口才能接受事件。 1.1.4 事件派送流程 事件派送流程包含如下几步: 1. 目标选择; 2. 路由构建; 3. 事件捕获; 4. 事件凸显 目标选择 当行为发生时,系统基于内部规则决定哪个节点是目标。对于键盘事件,目标就是节点 用于焦点;对于鼠标或滚动事件,目标就是节点所在的光标的位置。如光标所在的位置有多 个节点,则以最上的为目标。例如,如果用户点击图 1-2 中三角形,则三角形是目标而不是 包含三角形和圆形的矩形。当鼠标按下,则目标选定,所有后续鼠标事件被派送到相同目标, 直到按钮释放为止。 图 1-2 用户界面事件目标 路由构建 初始的事件路由有事件派发链表决定,链表是由选中的事件目标的 buildEventDispatchChain()方法实现。例如,如果用户点击了图 1-2 的三角形,则初始路由如 图 1-3 中灰色节点所示。当布景图作为事件目标时,初始事件路由开始于 Node 类的 buildEventDispatchChain()方法的缺省实现,即由 Stage 到自身。 图 1-3 事件派遣链表 在事件过滤器和事件处理器沿着路由处理事件时,路由可以被改变。也就是说,如果过滤器 或处理器在任一点销毁了事件,在初始路由上的接点就接受不到事件了。 事件捕获阶段 在事件捕获阶段,事件有应用根节点派发,并沿着派发链表向下传递到目标接点。图 1-3 使用了事件派发链表,在捕获阶段,事件遍历了从 Stage 节点到三角形节点。 如果链表中任何节点注册了相应事件类型的过滤器,则过滤器会被调用。当过滤完成, 事件被传递到链表中下一个节点。如果没有节点没有注册过滤器,则事件传递链表的下一个 节点。如果没有过滤器消费事件,则事件目标最终接受并处理事件。 事件凸显阶段 事件目标到达且注册过滤器已处理了事件,则事件沿着事件链表返回到根节点。如图 1-3 中的链表,在事件凸显阶段,事件从三角形遍历到 Stage 节点。 链表中任意节点为遇到的事件类型注册了处理器,则处理器会被调用。当处理器处理完 毕,事件向上返回下个节点。日光没有注册处理器,则直接返回上一节点。如果没有处理器 销毁事件,根节点最终接受到事件并完成处理。 1.1.5 事件处理 事件处理有过滤器和处理器提供,它们实现了 EventHandler 接口。如果需要在事件发生 时通知应用,注册一个事件过滤器或处理器。过滤器和处理器两者的主要不同是何时被执行。 事件过滤器 事件过滤器在事件捕获期间被执行。父节点的事件过滤器为多子节点提供一般的事件处 理,如果需要,可以销毁事件来阻止子节点接收事件。随着事件沿着注册了过滤器的节点传 递,过滤器为出现的注册事件类型执行。 一个节点可以注册多个过滤器。每个过滤器调用的次序基于事件的等级。特定过滤器优 先一般过滤器而执行。例如,MouseEvent.MOUSE_PRESSED 事件过滤器优先 InputEvent.ANY 事件而执行。同级别的过滤器执行顺序没有规定。 事件处理器 事件处理器在事件凸显阶段被执行。如果子节点的事件处理器没有销毁事件,父节点的 事件处理器将在子节点处理后执行事件,并且为多个子节点提供通用的事件处理。为出现的 事件类型注册的处理器,随事件经由注册了处理器的节点而被执行。 一个节点可以注册多个处理器,调用顺序基于事件类型的层级而定。特定类型的事件处 理器优先一般类型而执行。例如 KeyEvent.KEY_TYPED 处理器优先 InputEvent.ANY 而执行。 同级别的处理器执行次序没有特别规定。 销毁事件 事件可以有过滤器或处理器在事件链表人一点调用 consume()方法销毁。这个方法发生 时间完成处理的信号,然后事件派发链表遍历结束。 在过滤器中销毁事件会阻止链表中任意子节点执行事件。销毁处理器中的事件,会阻止 链表中父节点进一步处理事件。如果节点注册了多个过滤器或处理器,则对等的过滤器或处 理器依然会被执行。 注意:UI 控件的缺省处理器大都销毁了多数的输入事件。更多相关信息可以查看“JavaFX API 文档”。 1.2 使用便利的方法 本篇主题是描述在 JavaFX 应用中可以用于注册事件处理器的便利方法,即学习一种简 单的方法来创建和注册事件处理器,以对鼠标事件、键盘事件、行为事件、拖放事件、窗口 事件及其它做出响应。 有些 JavaFX 类定义了事件处理器特性,这些特性提供了注册事件处理器的方法。把事 件处理器特性配置到用户定义的处理器,以便注册处理器来接收处理相应的事件类型。为事 件处理器属性提供的 Setter 方法是一种便利的事件处理器注册方式。 1.2.1 使用便利方法 许多便利的方法在 Node 类中定义了,且对其所有子类都可用。其它的类也有便利的方 法。表 2-1 描述了能用于处理事件的便利方法以及识别哪个类中定义了便利方法。 表 2-1 有便利方法的事件处理器类 用户行为 事件类型 类 键盘按键 KeyEvent Node、Scene 鼠标移动或按键 MouseEvent Node、Scene 以交替方式输入字符,或生成、 改变、删除或提交 InputMethodEvent Node、Scene 拖动对象 DragEvent Node、Scene 滚动对象 ScrollEvent Node、Scene 按按钮或选中菜单项 ActionEvent ButtonBase,COntextMenu MenuItem,TextField 编辑清单、表或树的项 ListView.EditEvent; TableColumn.EditEvent TreeView.EditEvent ListView; TableColumn TreeView 播放器出错 MediaErrorEvent MediaView 菜单展示或隐藏 Event Menu 弹出窗口隐藏 Event PopupWindow Tab 选中或关闭 Event Tab 窗口关闭、展示或隐藏 WindowEvent Window 注册事件处理器的便利方法格式如下: setOnEvent-type(EventHandler value) 其中,Event-type 是事件处理器处理的事件类型,如 setOnKeyTyped 的 KEY_TYPED 事件,或 setOnMouseClicked 的 MOUSE_CLICKED 的事件;event_class 是定义事件类型的类,如 KeyEvent 是与键盘输入相关事件,MouseEvent 是鼠标输入相关事件;标明 event-class 或作为参数的超类事件处理器的接收事件处理器的方法。例如 InputEvent 的处理 器即可用于鼠标事件也可用于键盘事件。 下面语句展示了这种注册时间处理器处理按键时产生的事件的方法的定义,即按键或释放时: setOnKeyTyped(EventHandler value) 通过定义匿名处理器类来调用这种便利方法一步创建或注册事件处理器。事件处理器必须实 现 handle()方法,并提供相应的处理事件的代码。 便利方法应用示例如代码所示,代码用 Netbeans IDE 创建 JavaFX 应用产生。创建应用时, 如果选择“创建应用类(Create Application Class)”选项,创建的主类包含一个“Hello World” 应用。生成的代码如示例 2-1 所示。 示例 2-1 Hello World 例子 package yourapplication; import javafx.application.Application; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.stage.Stage; public class YourApplication extends Application { /** * @param args the command line arguments */ public static void main(String[] args) { Application.launch(args); } @Override public void start(Stage primaryStage) { primaryStage.setTitle("Hello World"); Group root = new Group(); Scene scene = new Scene(root, 300, 250); Button btn = new Button(); btn.setLayoutX(100); btn.setLayoutY(80); btn.setText("Hello World"); btn.setOnAction(new EventHandler() { public void handle(ActionEvent event) { System.out.println("Hello World"); } }); root.getChildren().add(btn); primaryStage.setScene(scene); primaryStage.show(); } } “Hello World”代码创建了带一个按钮的视窗。setOnAction()方法用于注册事件处理器,以 便处理按钮点击时派发的行为事件。处理器中 Handle()方法通过打印字符串“Hello World” 到控制台来处理事件。 鼠标事件示例 为鼠标事件注册处理器的便利方法包括 setOnMouseEntered 、 setOnMouseExited 和 setOnMousePressed。示例 2-2 展示了这些事件处理器的示例。 示例 2-2 鼠标事件处理器例子 final Circle circle = new Circle(radius, Color.RED); circle.setOnMouseEntered(new EventHandler() { public void handle(MouseEvent me) { System.out.println("Mouse entered"); } }); circle.setOnMouseExited(new EventHandler() { public void handle(MouseEvent me) { System.out.println("Mouse exited"); } }); circle.setOnMousePressed(new EventHandler() { public void handle(MouseEvent me) { System.out.println("Mouse pressed"); } }); 为明白应用的事件处理器如何相似,可以运行“Ensemble”示例,可以在此位置下载应用示 例捆绑包,其中的例子都有源代码。 键盘事件示例 为键盘准备的注册处理器的便利方法有 setOnKeyPressed 和 setOnKeyReleased,示例 2-3 展 示了这些处理器的应用示例。 示例 2-3 键盘事件处理器例子 final TextField textBox = new TextField(); textBox.setPromptText("Write here"); textBox.setOnKeyPressed(new EventHandler() { public void handle(KeyEvent ke) { System.out.println("Key Pressed: " + ke.getText()); } }); textBox.setOnKeyReleased(new EventHandler() { public void handle(KeyEvent ke) { System.out.println("Key Released: " + ke.getText()); } }); 相关示例也可在“Ensemble ”中查看。其它相关内容也可查看相关的 API 文档。 1.3 使用事件过滤器 这部分描述 JavaFX 应用中事件过滤器相关内容。学习过滤器如何用于处理由键盘、鼠标、 滚动以及其交互行为产生的事件。 事件过滤器使你能在事件处理的捕获阶段期间来处理事件。一个节点对象可以有多于一个的 事件处理过滤器。单个过滤器可以用于多个节点或多个事件类型。过滤器使父节点能为子节 点提供一个通用的处理或拦截一个事件,并阻止子节点执行相应事件。 1.3.1 注册和删除事件过滤器 为在事件捕获节点处理事件,节点必须注册一个事件过滤器。事件过滤器实现了 EventHandler 接口。事件由注册过滤器的节点接收,实现事件与过滤器关联,由 Handle()方 法内提供执行代码做相关处理。 为了注册过滤器,使用 addEventFilter 方法。这个方法有事件类型和过滤器两个参数。在示 例 3-1 中,第一个过滤器添加到单个节点并处理了一个特定类型事件。第二个过滤器处理输 入事件,由两个不同节点注册。相同的过滤器也可注册到两个不同类型的事件上。 示例 3-1 注册过滤器 // Register an event filter for a single node and a specific event type node.addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler() { public void handle(MouseEvent) { ... }; }); // Define an event filter EventHandler filter = new EventHandler(() { public void handle(InputEvent event) { System.out.println("Filtering out event " + event.getEventType()); event.consume(); } // Register the same filter for two different nodes myNode1.addEventFilter(MouseEvent.MOUSE_PRESSED, filter); myNode2.addEventFilter(MouseEvent.MOUSE_PRESSED, filter); // Register the filter for another event type myNode1.addEventFilter(KeyEvent.KEY_PRESSED, filter); 注意:为一个事件类型定义的过滤器也可用于事件的任意子类。查看“事件类型”了解事件 类型层级相关信息。 当不再需要过滤器来处理节点事件或事件类型是,可以用 removeEventFilter()方法来删 除过滤器。这个方法需要一个事件类型和过滤器作为参数。在示例 3-2 中,示例 3-1 中为 myNode1 定义的 MouseEvent.MOUSE_PRESSED 事件过滤器被删除。而 myNode2 和没有 Node1 的 KeyEvent.KEY_PRESSED 事件仍会执行。 示例 3-2 删除过滤器 // Remove an event filter myNode1.removeEventFilter(MouseEvent.MOUSE_PRESSED, filter); 1.3.2 使用事件过滤器 事件过滤器典型应用于时间派发链表的分支节点上,并在事件处理的捕获期间被调用。过滤 器的作用是执行诸如覆盖某事件响应或阻塞事件到达相应目的。可以下载 “DraggablePanelsExample.zip”开看过滤器如何应用的。下面部分将描述一下这个示例。 可拖动面板例子 可拖动面板示例演示了过滤器的如下用法:  为超类型事件注册过滤器并为子类事件提供通用处理;  销毁事件来阻止子节点执行。 图 3-1 截屏展示了可拖动面板示例启动的情形。用户界面有三个面板组成,每个面板包括不 同的 UI 控件。在底部是一个选择框,由它来控制面板是否可拖动。 图 3-1 可拖动面板的初始屏幕 如果选择框没选中,点击任何界面控件都会产生相应响应。如果选择框选中了,则个别控件 不会响应鼠标点击。相反,点击面板内任意地方并拖动鼠标移动整个面板,将可改变面板的 位置,如图 3-2 所示。 图 3-2 可重定位面板界面 可拖动面板过滤器示例 在可拖动面板示例中,makeDraggable()方法用于创建面板,并使每个面板可移动。这个 方法和过滤器在示例 3-3 中被定义。 示例 3-3 在 makeDraggable()定义过滤器 private Node makeDraggable(final Node node) { final DragContext dragContext = new DragContext(); final Group wrapGroup = new Group(node); wrapGroup.addEventFilter( MouseEvent.ANY, new EventHandler() { public void handle(final MouseEvent mouseEvent) { if (dragModeActiveProperty.get()) { // disable mouse events for all children mouseEvent.consume(); } } }); wrapGroup.addEventFilter( MouseEvent.MOUSE_PRESSED, new EventHandler() { public void handle(final MouseEvent mouseEvent) { if (dragModeActiveProperty.get()) { // remember initial mouse cursor coordinates // and node position dragContext.mouseAnchorX = mouseEvent.getX(); dragContext.mouseAnchorY = mouseEvent.getY(); dragContext.initialTranslateX = node.getTranslateX(); dragContext.initialTranslateY = node.getTranslateY(); } } }); wrapGroup.addEventFilter( MouseEvent.MOUSE_DRAGGED, new EventHandler() { public void handle(final MouseEvent mouseEvent) { if (dragModeActiveProperty.get()) { // shift node from its initial position by delta // calculated from mouse cursor movement node.setTranslateX( dragContext.initialTranslateX + mouseEvent.getX() - dragContext.mouseAnchorX); node.setTranslateY( dragContext.initialTranslateY + mouseEvent.getY() - dragContext.mouseAnchorY); } 下列事件定义的过滤器注册到每个面板上: MouseEvent.ANY:过滤器处理面板的所有鼠标事件。如果选择框选中,过滤器销毁事件, 则面板内的子节点不能接收事件;如果没选中,则鼠标位置的控件将处理相应事件。 MouseEvent.MOUSE_PRESSED:过滤器仅处理鼠标的按下事件。如果拖动模 式选择框选中,鼠标当前位置被存储。 MouseEvent.MOUSE_DRAGGED:过滤器只处理鼠标的面板拖动事件。如果拖动模式被选, 则面板是可移动的。 注意:一个面板有三个注册过滤器。特定事件类型的过滤器在超类事件前被调用。因此 MouseEvent.MOUSE_PRESSED and MouseEvent.MOUSE_DRAGGED 在 MouseEvent.ANY 过滤器前被调用。 1.4 使用时间处理器 本部分主题是描述 JavaFX 应用中事件处理器相关内容。在此将学习事件处理器如何处 理由键盘行为、鼠标行为、滚动行为以及用户交互产生的事件。 事件处理器使你在事件凸显(event bubbling)阶段就能处理相关事件。一个节点对象 可以有多余一个的事件处理器来处理一个事件。单个事件处理器能用于多个节点和多个事件 类型。如果子节点的事件处理器没有销毁事件,则父节点处理器使父节点在子节点处理后来 执行相关事件,并为多个子节点提供公共的事件处理。 1.4.1 注册和删除事件处理器 在事件凸显阶段处理事件,节点必须注册事件处理器。事件处理器就是 EventHandler 接口的实现。在接口的 handle()方法内提供相关代码,以便注册处理器的节点接收到事件时 执行。 使用 addEventHandler()方法来注册处理器。此方法以事件类型和处理器为参数。在示例 4-1 中,第一个处理器添加到单节点并处理特定类型事件;第二个处理器处理输入事件,并 注册到两个不同节点。相同的节点也可注册到不同类型的事件上。 示例 4-1 注册处理器 } }); return wrapGroup; } // Register an event handler for a single node and a specific event type node.addEventHandler(DragEvent.DRAG_ENTERED, new EventHandler() { public void handle(DragEvent) { ... }; }); // Define an event handler EventHandler handler = new EventHandler(() { public void handle(InputEvent event) { System.out.println("Handling event " + event.getEventType()); event.consume(); } // Register the same handler for two different nodes myNode1.addEventHandler(DragEvent.DRAG_EXITED, handler); myNode2.addEventHandler(DragEvent.DRAG_EXITED, handler); // Register the handler for another event type myNode1.addEventHandler(MouseEvent.MOUSE_DRAGGED, handler); 注意:为一种类型事件定义的事件处理器也可用于该事件的任何子类。有关“事件类型” 信息可以查阅前面相应文档描述。 当不再需要为某节点或类型事件做处理时,使用 removeEventHandler 方法删除处理器。此 方法需要事件类型和处理器作为参数。在示例 4-2 中演示了删除示例 4-1 中的 myNode1 的 DragEvent.DRAG_EXITED 事件处理器。myNode2 和 myNode1 的 MouseEvent.MOUSE_DRAGGED 事件依然被执行。 示例 4-2 删除处理器 // Remove an event handler myNode1.removeEventHandler(DragEvent.DRAG_EXITED, handler); 技巧:删除通过便利方法注册的处理器,只需要传 null 给便利方法即可,如 node1.setOnMouseDragged(null). 1.4.2 使用事件处理器 事件处理器典型应用于叶子节点或事件派发链表的分支上,且在处理的事件的凸显阶段。 在分支节点上使用处理器执行的动作为所有子节点定义的缺省响应。下面将通过示例展示处 理器如何使用的。 键盘示例(Keyboard Example) 键盘示例演示了如下处理器的使用:  为两个不同事件类型注册单一处理器;  为父节点的所有子节点提供公共事件处理。 图 4-1 展示键盘示例启动时的屏幕界面。用户界面有 4 个字母组成,且在自有的方形内表示 相应的键盘键。第一个是高亮的,表明具有焦点。使用键盘上左右箭头来移动焦点。 图 4-1 键盘示例初始屏幕 当“Enter”键按下,屏幕上有焦点的的变为红色,但“Enter”键释放,则恢复到原先颜色。 当按键与屏幕上的键匹配时,屏幕上匹配字母则变红色,而释放后又恢复原色。若按键没有 和屏幕匹配的,则什么都不发生。图 4-2 展示了当 A 有焦点和 D 键按下时的情景。 图 4-2 按键界面 键盘处理器示例 在键盘示例中,界面展示的每个键代表一个键节点。所有的键节点包含在单一的键盘节点。 每个键节点有个处理器以便键有简单时接收键事件。处理器对按键和“Enter”释放键做出 响应以使屏幕上键的颜色改变。然后事件被销毁,以便键盘节点(父节点)不接收事件。 示例 4-3 展示定义键节点处理器的 installEventHandler 方法情况。 示例 4-3 键节点处理器 private void installEventHandler(final Node keyNode) { // handler for enter key press / release events, other keys are // handled by the parent (keyboard) node handler final EventHandler keyEventHandler = new EventHandler() { public void handle(final KeyEvent keyEvent) { if (keyEvent.getCode() == KeyCode.ENTER) { setPressed(keyEvent.getEventType() == KeyEvent.KEY_PRESSED); keyEvent.consume(); } } }; keyNode.setOnKeyPressed(keyEventHandler); keyNode.setOnKeyReleased(keyEventHandler); } 键盘节点有两个处理器来处理没有被键节点销毁键事件。第一个处理器改变与按键匹配的键 节点颜色,第二个处理器响应左右键的移动和焦点。 示例 4-4 展示了定义键盘节点处理器的 installEventHandler 方法。 示例 4-4 键盘节点处理器 private void installEventHandler(final Parent keyboardNode) { // handler for key pressed / released events not handled by // key nodes final EventHandler keyEventHandler = new EventHandler() { public void handle(final KeyEvent keyEvent) { final Key key = lookupKey(keyEvent.getCode()); if (key != null) { key.setPressed(keyEvent.getEventType() == KeyEvent.KEY_PRESSED); keyEvent.consume(); } } }; keyboardNode.setOnKeyPressed(keyEventHandler); keyboardNode.setOnKeyReleased(keyEventHandler); keyboardNode.addEventHandler(KeyEvent.KEY_PRESSED, new EventHandler() { public void handle( final KeyEvent keyEvent) { handleFocusTraversal( keyboardNode, keyEvent); } }); } 按键事件的两个处理器是对等处理器。因此即便销毁了一个处理器,另一个仍会被调用。
还剩15页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享pdf获得金币 ] 1 人已下载

下载pdf

pdf贡献者

pmmf1243

贡献于2013-10-23

下载需要 8 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf