WPF中动态拓扑图节点位置记录教程

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本教程详细介绍了如何使用WPF技术创建一个包含节点拖动和位置记忆功能的拓扑图。通过实现自定义行为类和数据模型,学习如何处理节点拖动事件以及如何保存和恢复节点位置。教程还涉及了节点数据的存储和加载过程,最终目标是构建一个具备良好用户体验的交互式拓扑图应用。 拓扑图学习

1. WPF技术基础与拓扑图概述

1.1 WPF技术简介

WPF(Windows Presentation Foundation)是微软推出的一种基于.NET框架的用户界面框架,它允许开发者创建具有丰富视觉效果的桌面应用程序。WPF使用XAML(Extensible Application Markup Language)来描述用户界面,这种标记语言与HTML类似,但它更适合于定义复杂的用户界面和数据绑定。WPF的一个核心特性是它将UI的定义与逻辑处理分离,这种分离提高了可维护性和可扩展性。

1.2 WPF中XAML的作用

XAML作为一种声明性标记语言,使得UI的设计可以与后台代码逻辑分离。它能够定义布局、数据绑定以及资源等。在XAML中,可以使用各种控件,如按钮、文本框和图像等,并且可以利用样式和模板来统一控件外观。XAML的这种声明式特性使得开发人员可以直观地看到界面布局,并且容易进行修改和扩展。

1.3 拓扑图的基本概念

拓扑图是一种图形表示方法,用于描述网络中节点和连接线的关系。在计算机网络领域,拓扑图常被用来表示网络的物理或逻辑结构。它可以帮助网络管理员可视化网络资源,以及监控和管理网络状态。在WPF应用程序中,拓扑图通常是以图形化的方式在界面上表现出来,例如,可以展示网络中的服务器、交换机和连接关系等。而WPF技术为实现动态的、交互式的拓扑图提供了强大的支持。

2. 拓扑图节点拖动实现

拓扑图作为表示网络、系统架构或数据流等多种数据关系的图形,其用户交互友好性直接关系到整体体验。实现拓扑图节点的拖动功能,可以提升用户在可视化编辑中的灵活性。本章重点讨论在WPF(Windows Presentation Foundation)环境下实现拓扑图节点拖动的机制,并深入分析拖动过程中的技术细节。

2.1 WPF中控件的拖动基础

2.1.1 理解拖动操作的基本概念

在WPF中,拖动操作涉及到几个核心概念:鼠标捕获(Mouse Capture)、鼠标事件(Mouse Events)和拖动事件(Drag Events)。鼠标捕获是为了防止鼠标事件在拖动过程中丢失,它确保即使鼠标指针移动到控件之外,相关控件依然可以接收事件。鼠标事件提供了拖动操作开始和进行中的信息,如鼠标按下、移动等。拖动事件则是在拖动操作中特定的事件,例如拖动开始(PreviewMouseLeftButtonDown)和拖动结束(MouseLeftButtonUp)。

2.1.2 使用MouseEventArgs获取鼠标信息

在处理拖动相关的事件时,MouseEventArgs是获取鼠标信息的关键。通过MouseEventArgs,可以获得当前鼠标位置、按键状态等信息,从而在拖动过程中控制节点的移动。以下是一个简单的示例代码块,用于说明如何在事件处理函数中使用MouseEventArgs。

private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    // 获取鼠标位置
    Point mousePosition = e.GetPosition(this.canvas);
    // 根据鼠标位置获取目标节点
    Node node = FindNodeAtPosition(mousePosition);
    if (node != null)
    {
        // 设置鼠标捕获以防止事件丢失
        this.canvas.CaptureMouse();
        // 保存起始拖动位置
        this.lastMousePosition = mousePosition;
        // 显示拖动操作的视觉反馈
        node.IsSelected = true;
    }
}

在上述代码中,我们首先通过调用GetPositon方法获取鼠标相对于画布的位置,然后查找位于该位置的节点。一旦找到节点并确认开始拖动,我们通过调用CaptureMouse方法捕获鼠标,以便即使鼠标移出画布,依然能够接收到后续的鼠标移动事件。同时,我们也保存了鼠标起始位置,并且为被选中的节点提供了视觉反馈。

2.2 拓扑图节点的拖动逻辑

实现拓扑图节点的拖动功能涉及更复杂的逻辑,包括对节点的拖动处理、边界检测、以及节点重叠等问题的解决。

2.2.1 实现节点的基本拖动功能

节点的拖动功能通常需要以下步骤:

  1. 事件绑定 :为节点绑定鼠标事件,如MouseLeftButtonDown、MouseMove和MouseLeftButtonUp。
  2. 拖动开始 :捕获鼠标并记录起始位置。
  3. 拖动进行中 :在MouseMove事件中更新节点位置。
  4. 拖动结束 :释放鼠标捕获并更新最终位置。

节点的拖动实现涉及到如何记录和更新节点的位置信息。节点位置通常在Canvas或Grid布局控件中控制。以下是一个简化的示例代码块,用于说明如何在MouseMove事件中更新节点位置。

private void Canvas_MouseMove(object sender, MouseEventArgs e)
{
    if (this.canvas.IsMouseCaptured)
    {
        // 计算移动距离
        Vector delta = e.GetPosition(this.canvas) - this.lastMousePosition;
        // 更新节点位置
        node.Left += delta.X;
        *** += delta.Y;
        // 更新最后的鼠标位置
        this.lastMousePosition = e.GetPosition(this.canvas);
    }
}

在此代码中,我们计算了鼠标从上一次记录位置到当前位置的移动向量。然后更新节点的位置属性,使得节点随着鼠标的移动而移动。同时,我们也更新了最后鼠标位置的记录,以备下次移动时使用。

2.2.2 节点拖动的边界检测与处理

当节点在画布上被拖动时,需要考虑到它可能移动到画布之外,或者与其他节点重叠的问题。为了解决这些问题,我们可以在MouseMove事件处理函数中加入边界检测的逻辑。

private void Canvas_MouseMove(object sender, MouseEventArgs e)
{
    // ... [省略其他代码] ...
    // 检查边界
    if (node.Left < 0) node.Left = 0;
    if (*** < 0) *** = 0;
    if (node.Left + node.Width > this.canvas.ActualWidth) node.Left = this.canvas.ActualWidth - node.Width;
    if (*** + node.Height > this.canvas.ActualHeight) *** = this.canvas.ActualHeight - node.Height;
    // ... [省略其他代码] ...
}

在此代码段中,我们首先检查节点的新位置是否超出了画布的边界,并据此调整节点位置,确保它不会移动到画布之外。

接下来,我们还需要检查节点是否与其他节点重叠,并作出相应的处理。为简化示例,我们将使用一个简单的逻辑来检查重叠情况,并将当前节点上移或左移以避免重叠。

private void AvoidOverlap(Node node)
{
    foreach (Node otherNode in canvas.Children.OfType<Node>())
    {
        if (node != otherNode && node.Bounds.IntersectsWith(otherNode.Bounds))
        {
            // 避免重叠:向左移动
            if (node.Left > otherNode.Left)
            {
                node.Left -= node.Width;
            }
            // 避免重叠:向上移动
            else
            {
                *** -= node.Height;
            }
            break;
        }
    }
}

通过上述检查和调整步骤,我们能够确保节点在拖动过程中不会超出画布边界,并且尽可能避免与其他节点重叠。

本章节详细介绍了WPF中拓扑图节点拖动的基本概念和实现逻辑。在下一章节中,我们将进一步深入节点位置记录与恢复机制的细节,以及如何在复杂的拓扑图中实现高效的数据管理。

3. 节点位置记录与恢复机制

3.1 节点位置数据结构的设计

3.1.1 定义节点位置的数据模型

在实现拓扑图节点位置的记录与恢复机制时,首先需要定义一个适合的数据模型来存储节点的位置信息。通常情况下,节点的位置信息可以通过其在二维空间中的X和Y坐标来表示。为了能够满足更复杂的需求,还可以在数据模型中添加额外的信息,比如节点的宽度和高度、是否可移动、是否选中等状态信息。

下面是一个简单的数据模型定义示例:

public class NodePosition
{
    public double X { get; set; }
    public double Y { get; set; }
    public double Width { get; set; }
    public double Height { get; set; }
    public bool IsSelected { get; set; }
}

3.1.2 实现位置信息的存储与读取

在定义好数据模型后,我们需要实现位置信息的存储与读取机制。这通常涉及到文件系统、数据库或内存中的数据存储。在WPF应用程序中,为了实现节点位置的保存与恢复,可以使用序列化机制将位置信息保存到文件中,并在应用程序启动时进行反序列化。

下面是一个将节点位置信息保存到文件的代码示例:

using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
// ...

public void SaveNodePositions(IEnumerable<NodePosition> positions, string filePath)
{
    using (Stream stream = new FileStream(filePath, FileMode.Create))
    {
        BinaryFormatter formatter = new BinaryFormatter();
        formatter.Serialize(stream, positions);
    }
}

读取位置信息的代码示例:

public IEnumerable<NodePosition> LoadNodePositions(string filePath)
{
    if (!File.Exists(filePath))
    {
        return Enumerable.Empty<NodePosition>();
    }

    using (Stream stream = new FileStream(filePath, FileMode.Open))
    {
        BinaryFormatter formatter = new BinaryFormatter();
        IEnumerable<NodePosition> positions = formatter.Deserialize(stream) as IEnumerable<NodePosition>;
        return positions;
    }
}

3.2 节点位置的记录方法

3.2.1 通过事件监听记录位置变化

为了实时跟踪节点位置的变化,我们需要在节点的位置属性发生变化时记录新的位置。这通常通过监听节点的事件来实现,比如在WPF中可以通过监听控件的 PreviewMouseMove MouseMove 事件来实现位置的实时跟踪。

下面是一个示例代码,展示了如何在 MouseMove 事件中更新节点的位置信息:

private void Node_MouseMove(object sender, MouseEventArgs e)
{
    NodePosition position = sender as NodePosition;
    if (position != null)
    {
        Point currentPos = e.GetPosition(this);
        position.X = currentPos.X;
        position.Y = currentPos.Y;
        // 更新节点的UI位置
        Canvas.SetLeft(Node, currentPos.X);
        Canvas.SetTop(Node, currentPos.Y);
    }
}

3.2.2 位置信息的定时保存与恢复

为了确保数据的安全性,除了实时记录位置变化,还可以定时将位置信息保存到文件中,这样即使应用程序崩溃,用户的信息也不会丢失。同样地,在应用程序启动或节点恢复显示时,可以从文件中读取位置信息并恢复到对应的节点上。

一个定时保存位置信息的示例:

private Timer saveTimer;

public void InitializeSaveTimer()
{
    saveTimer = new Timer(10000); // 设置定时器每10秒触发一次
    saveTimer.Elapsed += (sender, e) => SaveNodePositions(nodesPositions, filePath);
    saveTimer.AutoReset = true;
    saveTimer.Enabled = true;
}

在应用程序关闭时应该停止定时器并执行最后一次保存:

public void ShutdownApplication()
{
    saveTimer.Enabled = false;
    SaveNodePositions(nodesPositions, filePath);
    // 其他清理代码...
}

以上,我们已经探讨了节点位置记录与恢复机制的设计和实现方式,确保了拓扑图节点位置信息的持久性和可靠性。

4. 自定义行为类(Behavior)创建与应用

4.1 创建自定义行为类(Behavior)

4.1.1 理解行为类的原理与用途

行为类(Behavior)是WPF中提供的一种扩展控件功能的强大机制,允许开发者将自定义行为附加到现有的控件上,而无需修改控件本身的代码。通过创建行为类,我们可以实现对控件行为的封装与复用,从而使得代码更加模块化和易于管理。行为类使用事件和附加属性与宿主控件通信,它实质上是封装了一系列逻辑的代码块,可以在特定的触发条件下执行。

行为类经常用于以下用途:

  • 为控件添加或增强交互行为,例如拖动、双击等。
  • 以一种更加灵活的方式扩展控件功能,而不是通过继承。
  • 实现跨平台的UI功能,行为类可以在不同的UI框架中被重用。

4.1.2 编写自定义行为类的步骤

创建一个自定义行为类需要遵循几个基本步骤:

  1. 定义行为类并实现 Behavior<T> 泛型类,其中 T 是目标控件的类型。
  2. 重写 OnAttached OnDetaching 方法,分别用于附加和分离行为到控件。
  3. 添加事件处理程序或附加属性,以响应控件的特定事件或状态变化。
  4. 编写逻辑代码以实现所需的行为功能。

下面是一个简单的行为类的代码示例,该行为类将在目标控件上监听鼠标点击事件,并在事件触发时输出一个信息:

using System.Windows.Interactivity;
using System.Windows;
using System.Windows.Input;

public class ClickLoggingBehavior : Behavior<UIElement>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.AddHandler(MouseLeftButtonDownEvent, new MouseButtonEventHandler(OnMouseButtonClicked), true);
    }

    protected override void OnDetaching()
    {
        AssociatedObject.RemoveHandler(MouseLeftButtonDownEvent, new MouseButtonEventHandler(OnMouseButtonClicked));
        base.OnDetaching();
    }

    private void OnMouseButtonClicked(object sender, MouseButtonEventArgs e)
    {
        MessageBox.Show("Click event triggered");
    }
}

在上述代码中, OnAttached 方法中我们为关联的 UIElement 添加了鼠标左键点击事件的处理器。在 OnDetaching 方法中,我们移除了事件处理器以防止内存泄漏。 OnMouseButtonClicked 方法是事件处理器,当点击事件触发时,会弹出一个包含消息的对话框。

4.2 行为类在节点拖动中的应用

4.2.1 将行为类应用到节点拖动功能

在WPF中实现拖动功能时,行为类可以提供一种灵活的方式来处理拖动逻辑。首先,我们需要创建一个自定义的行为类来处理拖动事件,然后将该行为附加到节点控件上。下面是一个拖动行为类的示例:

using System.Windows;
using System.Windows.Input;
using System.Windows.Interactivity;

public class DragBehavior : Behavior<UIElement>
{
    private Point _startPoint;
    private bool _isDragging;

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.PreviewMouseLeftButtonDown += AssociatedObject_PreviewMouseLeftButtonDown;
        AssociatedObject.PreviewMouseMove += AssociatedObject_PreviewMouseMove;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.PreviewMouseMove -= AssociatedObject_PreviewMouseMove;
        AssociatedObject.PreviewMouseLeftButtonDown -= AssociatedObject_PreviewMouseLeftButtonDown;
        base.OnDetaching();
    }

    private void AssociatedObject_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        if (e.ButtonState == MouseButtonState.Pressed)
        {
            _startPoint = e.GetPosition(null);
            _isDragging = false;
        }
    }

    private void AssociatedObject_PreviewMouseMove(object sender, MouseEventArgs e)
    {
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            var currentPosition = e.GetPosition(null);
            var vector = currentPosition - _startPoint;

            if (!_isDragging && (Math.Abs(vector.X) > SystemParameters.MinimumHorizontalDragDistance || Math.Abs(vector.Y) > SystemParameters.MinimumVerticalDragDistance))
            {
                _isDragging = true;
            }

            if (_isDragging)
            {
                var offset = new Point(currentPosition.X - _startPoint.X, currentPosition.Y - _startPoint.Y);
                Canvas.SetLeft(AssociatedObject, Canvas.GetLeft(AssociatedObject) + offset.X);
                Canvas.SetTop(AssociatedObject, Canvas.GetTop(AssociatedObject) + offset.Y);
                _startPoint = currentPosition;
            }
        }
    }
}

在这个行为类中,我们添加了两个事件处理器: PreviewMouseLeftButtonDown 用于捕获鼠标按下的位置,而 PreviewMouseMove 则用于在用户拖动时移动控件。通过计算鼠标移动的距离,我们可以判断是否开始拖动,并更新控件的位置。

要将此行为附加到节点控件,我们需要在XAML中使用 Interaction 命名空间,并将 DragBehavior 附加到相应的UIElement:

<Window xmlns:i="***">
    <Grid>
        <i:Interaction.Behaviors>
            <local:DragBehavior/>
        </i:Interaction.Behaviors>
        <Canvas>
            <Rectangle Width="50" Height="50" Fill="Blue" Canvas.Left="10" ***="10"/>
        </Canvas>
    </Grid>
</Window>

在这里, local 前缀指向了行为类的命名空间,并且我们添加了 DragBehavior 到一个位于 Canvas 中的 Rectangle

4.2.2 行为类中事件处理的优化策略

在实际应用中,事件处理需要进行优化以确保应用性能和用户体验。优化策略可能包括:

  • 避免在事件处理器中执行耗时的操作。如果需要执行这样的操作,考虑在另一个线程中执行。
  • 对于连续的事件(例如 MouseMove ),使用节流(throttling)或防抖(debouncing)技术以降低事件处理器的调用频率。
  • 当控件不再可见时,取消注册事件处理器,以减少不必要的事件处理。
  • 根据控件的需要动态添加和移除事件处理器。

例如,可以添加逻辑来限制 MouseMove 事件的处理次数:

private const int MouseMoveThrottleTime = 20; // milliseconds

private void AssociatedObject_PreviewMouseMove(object sender, MouseEventArgs e)
{
    if (!_throttleMouseMove)
    {
        _throttleMouseMove = true;
        Dispatcher.InvokeAsync(() => 
        {
            _throttleMouseMove = false;
            if (e.LeftButton == MouseButtonState.Pressed && !_isDragging && (Math.Abs(vector.X) > SystemParameters.MinimumHorizontalDragDistance || Math.Abs(vector.Y) > SystemParameters.MinimumVerticalDragDistance))
            {
                _isDragging = true;
            }

            if (_isDragging)
            {
                var offset = new Point(currentPosition.X - _startPoint.X, currentPosition.Y - _startPoint.Y);
                Canvas.SetLeft(AssociatedObject, Canvas.GetLeft(AssociatedObject) + offset.X);
                Canvas.SetTop(AssociatedObject, Canvas.GetTop(AssociatedObject) + offset.Y);
                _startPoint = currentPosition;
            }
        }, DispatcherPriority.ApplicationIdle);
    }
}

在上述代码中,我们使用了 Dispatcher.InvokeAsync 来确保耗时的UI操作在UI线程的空闲时间执行,并通过一个布尔变量 _throttleMouseMove 来限制 MouseMove 事件的处理频率。这种方法可以防止在用户快速移动鼠标时处理器过载。

通过创建自定义行为类并将其应用于节点拖动,我们不仅增强了控件的功能,还提高了代码的可维护性和可重用性。通过优化行为类中的事件处理,我们确保了应用能够以最佳性能运行,同时为用户提供流畅的交互体验。

5. 数据持久化与XAML节点控件定义

5.1 数据持久化的多种方法探讨

在WPF应用程序中,数据持久化是指将应用程序的数据存储到磁盘中,以便在应用程序关闭后再次打开时能够恢复数据。有几种常见的方法可以实现这一目标。

5.1.1 XML数据存储与解析

XML(可扩展标记语言)是一种通用的数据格式,常用于存储和传输数据。在.NET中,可以使用 XmlSerializer 类来序列化和反序列化对象。

XmlSerializer xmlSerializer = new XmlSerializer(typeof(NodeData));
using (TextWriter writer = new StreamWriter("nodeData.xml"))
{
    xmlSerializer.Serialize(writer, nodeDataInstance);
}

上述代码段演示了如何将一个名为 nodeDataInstance 的对象序列化为XML文件。在需要的时候,可以使用 XmlSerializer 将XML文件反序列化回对象。

5.1.2 JSON数据存储与解析

JSON(JavaScript对象表示法)是一种轻量级的数据交换格式,它比XML更简洁。在.NET中,可以使用 *** 库来处理JSON数据。

string jsonData = JsonConvert.SerializeObject(nodeDataInstance);
File.WriteAllText("nodeData.json", jsonData);

// 读取JSON数据并反序列化
NodeData nodeData = JsonConvert.DeserializeObject<NodeData>(jsonData);

这里的 JsonConvert.SerializeObject JsonConvert.DeserializeObject 方法分别用于将对象序列化为JSON字符串和将JSON字符串反序列化为对象。

5.1.3 数据库存储方案的选择与实现

对于更复杂的数据持久化需求,数据库是一个更好的选择。可以使用如SQLite、SQL Server等数据库系统。在.NET中,可以利用Entity Framework等ORM工具来简化数据库操作。

// 示例代码省略了具体的数据库配置和上下文定义
using (var context = new MyDbContext())
{
    context.Nodes.Add(nodeDataInstance);
    context.SaveChanges();
}

通过ORM工具,可以将对象直接保存到数据库中,并且在需要时从数据库中检索它们。

5.2 在XAML中定义节点控件

XAML是WPF中用于定义用户界面的语言。通过XAML,开发者可以设计出丰富的、具有良好布局的用户界面。

5.2.1 XAML布局基础与控件属性

控件是构成用户界面的基本元素。在XAML中定义节点控件涉及到设置合适的控件属性来满足布局和外观需求。

<UserControl x:Class="YourNamespace.NodeControl"
             xmlns="***"
             xmlns:x="***">
    <Grid>
        <Ellipse x:Name="NodeShape" Width="50" Height="50"
                 Fill="Blue" Stroke="Black"/>
        <TextBlock x:Name="NodeText" HorizontalAlignment="Center"
                   VerticalAlignment="Center" Text="Node"/>
    </Grid>
</UserControl>

上面的代码定义了一个具有椭圆形和文本的简单节点控件。

5.2.2 节点控件的样式与模板自定义

在WPF中,通过样式和控件模板可以定义和修改控件的外观和行为。

<Style TargetType="{x:Type local:NodeControl}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:NodeControl}">
                <Grid>
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                        <ContentPresenter Content="{TemplateBinding Content}"
                                          ContentTemplate="{TemplateBinding ContentTemplate}"/>
                    </Border>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

上面的 Style ControlTemplate 定义了一个控件模板,允许自定义节点控件的整体布局和外观。

5.3 后台代码与拖放逻辑处理

在WPF应用程序中,后台代码通常负责实现逻辑处理。当涉及到拖放操作时,需要理解MVVM模式的应用。

5.3.1 理解MVVM模式在拖放操作中的应用

MVVM(Model-View-ViewModel)模式通过使用数据绑定来分离用户界面逻辑和业务逻辑。在拖放操作中,ViewModel负责处理用户的拖放请求,并将这些操作转换为对Model的更改。

5.3.2 后台代码中拖放逻辑的实现与优化

为了实现拖放逻辑,需要在后台代码中处理相关的事件,如 MouseLeftButtonDown MouseMove

private void NodeControl_MouseDown(object sender, MouseButtonEventArgs e)
{
    // Start drag-drop operation
    if (e.ChangedButton == MouseButton.Left)
    {
        // Implementation of drag-drop handling
    }
}

优化拖放逻辑可以涉及减少不必要的重绘操作,或者优化数据的序列化与反序列化过程以提高效率。

以上就是第五章节的核心内容。通过本章节,我们讨论了数据持久化的多种方法、如何在XAML中定义节点控件,以及如何在后台代码中处理拖放逻辑。接下来,第六章将继续深入探讨如何优化WPF应用程序的性能表现。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本教程详细介绍了如何使用WPF技术创建一个包含节点拖动和位置记忆功能的拓扑图。通过实现自定义行为类和数据模型,学习如何处理节点拖动事件以及如何保存和恢复节点位置。教程还涉及了节点数据的存储和加载过程,最终目标是构建一个具备良好用户体验的交互式拓扑图应用。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值