WIN8 Metro风格应用开发框架的初步了解

今天按照微软提供的win8开发文档中的一个小例子:博客阅读器 ,完成了第一个win8 Metro风格的应用程序,以下是开发过程中比较重要的地方,对于熟悉整个框架有很大的帮助。

       其实我们需要知道在开发过程中 几个大的步骤是什么,先做什么 后做什么,再做什么,该文档非常清晰的阐述了这些步骤。

       第一步,应该考虑自己需要用到什么样的数据,哪些数据,数据的类型,以及我们需要这些数据去做什么,如何检索,如何获取,如何容纳。在这里,我们可以分别编写可以完成相应功能的类,而最终,我需要得到的是应用程序所需的资源。在示例里面,我们使用 3 个类来容纳和检索信息提要数据。我们将所有 3 个类都放在一个名为 FeedData(.cs 或 .vb)的文件中。FeedData 类容纳有关 RSS 或 Atom 信息提要的信息。FeedItem 类容纳有关信息提要所包含的单个博客文章的信息。FeedDataSource 类包含信息提要的机会以及从网络检索信息提要的方法。以下是这些类的代码。

     

namespace WindowsBlogReader
{
    // FeedData
    // Holds info for a single blog feed, including a list of blog posts (FeedItem)
    public class FeedData
    {
        public string Title { get; set; }
        public string Description { get; set; }
        public DateTime PubDate { get; set; }

        private List<FeedItem> _Items = new List<FeedItem>();
        public List<FeedItem> Items
        {
            get
            {
                return this._Items;
            }
        }
    }

    // FeedItem
    // Holds info for a single blog post
    public class FeedItem
    {
        public string Title { get; set; }
        public string Author { get; set; }
        public string Content { get; set; }
        public DateTime PubDate { get; set; }
        public Uri Link { get; set; }
    }

    // FeedDataSource
    // Holds a collection of blog feeds (FeedData), and contains methods needed to
    // retreive the feeds.
    public class FeedDataSource
    {
        private ObservableCollection<FeedData> _Feeds = new ObservableCollection<FeedData>();
        public ObservableCollection<FeedData> Feeds
        {
            get
            {
                return this._Feeds;
            }
        }

        public async Task GetFeedsAsync()
        {
            Task<FeedData> feed1 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/developers/atom.aspx");
            Task<FeedData> feed2 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/windowsexperience/atom.aspx");
            Task<FeedData> feed3 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/extremewindows/atom.aspx");
            Task<FeedData> feed4 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/business/atom.aspx");
            Task<FeedData> feed5 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/bloggingwindows/atom.aspx");
            Task<FeedData> feed6 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/windowssecurity/atom.aspx");
            Task<FeedData> feed7 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/springboard/atom.aspx");
            Task<FeedData> feed8 = 
                GetFeedAsync("http://windowsteamblog.com/windows/b/windowshomeserver/atom.aspx");
            // There is no Atom feed for this blog, so we use the RSS feed.
            Task<FeedData> feed9 = 
                GetFeedAsync("http://windowsteamblog.com/windows_live/b/windowslive/rss.aspx");
            Task<FeedData> feed10 = 
                GetFeedAsync("http://windowsteamblog.com/windows_live/b/developer/atom.aspx");
            Task<FeedData> feed11 = 
                GetFeedAsync("http://windowsteamblog.com/ie/b/ie/atom.aspx");
            Task<FeedData> feed12 = 
                GetFeedAsync("http://windowsteamblog.com/windows_phone/b/wpdev/atom.aspx");
            Task<FeedData> feed13 = 
                GetFeedAsync("http://windowsteamblog.com/windows_phone/b/wmdev/atom.aspx");

            this.Feeds.Add(await feed1);
            this.Feeds.Add(await feed2);
            this.Feeds.Add(await feed3);
            this.Feeds.Add(await feed4);
            this.Feeds.Add(await feed5);
            this.Feeds.Add(await feed6);
            this.Feeds.Add(await feed7);
            this.Feeds.Add(await feed8);
            this.Feeds.Add(await feed9);
            this.Feeds.Add(await feed10);
            this.Feeds.Add(await feed11);
            this.Feeds.Add(await feed12);
            this.Feeds.Add(await feed13);
        }

        private async Task<FeedData> GetFeedAsync(string feedUriString)
        {
            // using Windows.Web.Syndication;
            SyndicationClient client = new SyndicationClient();
            Uri feedUri = new Uri(feedUriString);

            try
            {
                SyndicationFeed feed = await client.RetrieveFeedAsync(feedUri);

                // This code is executed after RetrieveFeedAsync returns the SyndicationFeed.
                // Process it and copy the data we want into our FeedData and FeedItem classes.
                FeedData feedData = new FeedData();

                feedData.Title = feed.Title.Text;
                if (feed.Subtitle != null && feed.Subtitle.Text != null)
                {
                    feedData.Description = feed.Subtitle.Text;
                }
                // Use the date of the latest post as the last updated date.
                feedData.PubDate = feed.Items[0].PublishedDate.DateTime;

                foreach (SyndicationItem item in feed.Items)
                {
                    FeedItem feedItem = new FeedItem();
                    feedItem.Title = item.Title.Text;
                    feedItem.PubDate = item.PublishedDate.DateTime;
                    feedItem.Author = item.Authors[0].Name.ToString();
                    // Handle the differences between RSS and Atom feeds.
                    if (feed.SourceFormat == SyndicationFormat.Atom10)
                    {
                        feedItem.Content = item.Content.Text;
                        feedItem.Link = new Uri("http://windowsteamblog.com" + item.Id);
                    }
                    else if (feed.SourceFormat == SyndicationFormat.Rss20)
                    {
                        feedItem.Content = item.Summary.Text;
                        feedItem.Link = item.Links[0].Uri;
                    }
                    feedData.Items.Add(feedItem);
                }
                return feedData;
            }
            catch (Exception)
            {
                return null;
            }
        }
    }
}


检索信息提要数据

下面让我们更进一步了解如何下载博客源。Windows.Web.Syndication.SyndicationClient 类检索完全分析的 RSS 或 Atom 源,因此我们可以使用数据,而不必担心分析 XML。 若要使用 SyndicationClient 类下载源,我们必须使用异步的 RetrieveFeedAsync 方法。在 Windows 运行时中常经常使用异步编程模型来帮助应用保持响应。幸运的是,程序已经为我们处理好了在使用异步方法时可能会遇到的许多复杂问题。

在 C# 和 Visual Basic 中使用 await

如果在 C# 和 Visual Basic 中使用 await 关键字,则以异步方式检索信息提要的代码将与以同步方式检索信息提要时所使用的代码相似。

SyndicationFeed feed = await client.RetrieveFeedAsync(feedUri);

此处的 await 关键字会告诉编译器在后台自动为我们执行多种处理。编译器会将该方法中位于此调用之后的其余部分作为此调用返回后将要执行的回调。它会紧接着将控制返回给调用会话(通常是 UI 会话),以使应用保持响应。此时,会将表示此方法的最终输出的 TaskFeedData 对象)返回给调用者。

RetrieveFeedAsync 返回包含我们所需数据的 SyndicationFeed 时,将执行我们的方法中其余的代码。重要的是,系统在我们从中进行原始调用的相同会话上下文(UI 会话)中执行这些代码,因此当我们需要在此代码中更新 UI 时不必担心使用调度程序。检索到 SyndicationFeed 后,我们将需要的部分复制到 FeedDataFeedItem 数据类中。

而这个数据类可以为UI的控件提供需要的应用资源!,第一步,我们就算完成了。

我想这一部分的内容在以后的开发过程中还是非常重要的。

第二步 向应用中添加资源的步骤

  1. 在“解决方案资源管理器”中,双击 App.xaml。随即将在 XAML 编辑器中打开该文件。
  2. MergedDictionaries 集合中添加 <ResourceDictionary> </ResourceDictionary> 标记。
  3. 将资源声明 <local:FeedDataSource x:Key="feedDataSource"/> 添加到新的 ResourceDictionary 中。

页面模板已经在它的代码隐藏文件中包含一个 OnNavigatedTo 方法的替代。我们将代码置于此方法中,以获得应用的 FeedDataSource 实例并获得源。首先,我们将 async 关键字添加到方法声明,因为我们在方法中使用 await 关键字。导航到页面时,我们检查以查看 FeedDataSource 是否已包含源。如果未包含,我们调用 FeedDataSource.GetFeedsAsync 方法。然后,将页面的 DataContext 设置为第一个源。 以下是 MainPage.xaml.cs/vb 的相关代码。

  protected override async void OnNavigatedTo(NavigationEventArgs e)
        {
            FeedDataSource feedDataSource = (FeedDataSource)App.Current.Resources["feedDataSource"];
            
            if (feedDataSource != null)
            {
                if (feedDataSource.Feeds.Count == 0)
                {
                    await feedDataSource.GetFeedsAsync();
                }
                this.DataContext = (feedDataSource.Feeds).First();
            }
        }

这样,我们就将之前我们所容纳的数据添加到页面,此时,XAML中可以使用这些资源。

第三步:在XAML中定义应用布局:

         其实我个人觉得 UI的设计可以放在最前面,这样的步骤我觉得适合有长期开发经验的人员,我是菜鸟,所以我认为,我没有那么准确的能够知道所有的数据资源,以及这个应用中,哪些是该用到的,用在哪个布局中,哪个地方需要引用哪些数据资源。这些都是比较复杂的地方,所以我觉得初学者可以先尝试着去设计粗略的UI界面,根据自己的UI界面,再去思考如何去对应完成功能。这样虽然效率不够高,但是也相对简单。或者你可以先自己制作一个布局模板,再按照这个步骤来。我觉得只要有耐心都是可以的。


UI布局XAML代码如下

<Page
    x:Class="WindowsBlogReader.MainPage"
    IsTabStop="false"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WindowsBlogReader"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="140" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <!-- Title -->
        <TextBlock x:Name="TitleText" Text="{Binding Title}"
                   VerticalAlignment="Center" FontSize="48" Margin="56,0,0,0"/>

        <!-- Content -->
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="2*" MinWidth="320" />
                <ColumnDefinition Width="3*" />
            </Grid.ColumnDefinitions>

            <!-- Left column -->
            <!-- The default value of Grid.Column is 0, so we do not need to set it   
                 to make the ListView show up in the first column. -->
            <ListView x:Name="ItemListView"  
                      ItemsSource="{Binding Items}"
                      Margin="60,0,0,10">
            </ListView>

            <!-- Right column -->
            <!-- We use a Grid here instead of a StackPanel so that the WebView sizes correctly. -->
            <Grid DataContext="{Binding ElementName=ItemListView, Path=SelectedItem}"
                  Grid.Column="1" Margin="25,0,0,0">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="*" />
                </Grid.RowDefinitions>
                <TextBlock x:Name="PostTitleText" Text="{Binding Title}" FontSize="24"/>
                <WebView x:Name="ContentView" Grid.Row="1" Margin="0,5,20,20"/>
            </Grid>
        </Grid>
    </Grid>
</Page>

第一个行定义(行 0)上的 Height="140" 属性设置将顶部的行设置为 140 个与设备无关的像素的绝对高度。无论行的内容或应用的大小如何变化,此高度都不会改变。第二个行定义(第 1 行)中的 Height="*" 设置指示底部行接受第 0 行确定大小后剩余的任意大小的空间。这通常称为星形比例缩放。我们在第二个 Grid 的列定义中也使用了比例缩放。Width="2*" 的宽度设置和 Width="3*" 要求将 Grid 自身分为 5 个相等的部分。两个部分用于第一列,三个部分用于第二列。

若要在 Grid 中定位某个元素,则需要设置该元素的 Grid.Row Grid.Column 附加属性。行和列编号是从零开始的。这些属性的默认值是 0,因此如果未设置任何内容,则该元素将位于第一行第一列。

<Grid Grid.Row="1"> 元素在根 Grid 的底部行嵌入一个 Grid。该 Grid 被划分为两列。

元素 <ListView x:Name="ItemListView"> 将一个 ListView 添加到底部 Grid 的左侧列中。元素 <Grid Grid.Column="1"> 将另一个 Grid 添加到底部 Grid 的右侧列中。我们将该 Grid 划分为两行。设置 Height="Auto" 要求顶行尽可能地调整高度以适合其内容。底行则占用剩下的所有空间。

到了这里,界面初始运行的效果就会出来了。只是没有资源数据的显示内容,最后一步,也就是显示部分了。

第四步:显示数据

显示数据,也就是将UI控件与数据资源相关联起来,这里需要了解绑定。

在上述XAML文件中,已经给出如何绑定源对象的代码:如

<TextBlock x:Name="TitleText" Text="{Binding Title}"
           VerticalAlignment="Center" FontSize="48" Margin="56,0,0,0" />


<TextBlock x:Name="PostTitleText" Text="{Binding Title}" FontSize="24"/>


这里出现了一个很难理解的问题,如果两个 TextBlock 都绑定到同一个源对象,那它们如何显示不同的标题呢?答案就在每个 TextBlock 所绑定的 DataContext 中。使用 DataContext 属性可设置整个 UI 元素的默认绑定,包括其所有子元素。有时你为整个页面设置 DataContext 属性,而有时你需要为页面上的个别元素设置该属性。每个 XAML 级别的 DataContext 设置都会替代更高一级的所有设置。此外,你还可以通过设置个别绑定的 Source 属性来替代针对该绑定的任何已生效的 DataContext 设置。

第一个 TextBlock 的上下文是 FeedData 对象,因此它显示 FeedData.Title 属性。

第二个 TextBlock 如何显示所选博客文章的标题?如下所示,第二个 TextBlock 位于一个 Grid中。

<Grid DataContext="{Binding ElementName=ItemListView, Path=SelectedItem}" 
      Grid.Column="1" Margin="25,0,0,0">
      ...
      <TextBlock x:Name="PostTitleText" Text="{Binding Title}" FontSize="24"/>
      ...
</Grid>


Grid DataContext ListView SelectedItem 属性绑定。因此绑定引擎将再次为我们执行后台操作。只要 ListView 中的选择发生变化,StackPanelDataContext 就会自动更新为所选文章。GridDataContext 会替代页面的 DataContext,因此第二个 TextBlock 将显示所选博客文章的 FeedItem.Title 属性。

每个绑定都具有 Mode 属性,用于指定数据更新的方式和时间。

绑定模式描述
OneTime仅当第一次创建绑定时才在目标上设置该值,但以后不会再更新该值。
OneWay如果源更改,则会更新目标。
TwoWay如果目标或源中的一个更改,则会更新二者。

如果你使用 OneWayTwoWay 绑定,若要将源对象的更改通知绑定,则必须实现 INotifyPropertyChanged 接口。有关数据绑定的详细信息,请参阅快速入门:与控件绑定的数据

在 WebView 中显示 HTML

在页面中显示博客文章的最后一步是,获取文章数据以在 WebView 控件中显示。我们已经在我们的 UI 中添加了这一控件 ContentView

<WebView x:Name="ContentView" Grid.Row="1" Margin="0,5,20,20"/>


添加事件处理程序的步骤

  1. 在 XAML 或设计视图中,选择需要为其处理事件的控件。在此示例中,单击 MainPage.xaml 中的 ItemListView
  2. 在“属性窗口”中,单击“事件”按钮 (“事件”按钮)。

    提示  如果你没有看到“属性窗口”,则按 Alt+Enter 将其打开。

  3. 在事件列表中找到 SelectionChanged 事件。在该事件的文本框中,键入 "ItemListView_SelectionChanged"。

    “属性窗口”中的事件列表

  4. 按 Enter。随即会创建事件处理程序并在代码编辑器中打开。添加发生该事件时执行的代码。

我们向在代码隐藏页面中创建的事件处理程序中添加代码。在事件处理程序中,我们将所选项目转换为 FeedItem,并从 Content 属性获取 HTML 的字符串。然后,我们将该字符串传递给 NavigateToString 方法。如果没有选择任何项目,则我们会通过传递空字符串来清除 WebView

下面是添加的代码:

private void ItemListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
            // If there's a selected item (in AddedItems)
            // show it in the WebView.
            if (e.AddedItems.Count > 0)
            {
                FeedItem feedItem = e.AddedItems[0] as FeedItem;
                if (feedItem != null)
                {
                    // Navigate the WebView to the blog post content HTML string.
                    ContentView.NavigateToString(feedItem.Content);
                }
            }
            else
            {
                // If the item was de-selected, clear the WebView.
                ContentView.NavigateToString("");
            }
}


最后运行的结果为:

以下是该应用运行时的情况。

这是已完成的博客阅读器应用。

这是一个不错的基本页面,在大多数情况下可以正常运行。但是 Metro 风格应用需要在所有情况下都能够运行良好。它必须适应各种设备上不同

小结:

整个程序的逻辑非常清楚,而且非常明确的指出了初学者在以后的学习过程中会遇到的难以理解的问题。通过这一章的学习,对整个框架有了初步的了解与认识。

在这一章中,我还觉得有一个以后经常会用到的知识点:

使用值转换器设置数据格式

ItemListView DataTemplate 中,我们将 PubDate 属性(为 DateTime)绑定到 TextBlock.Text 属性。绑定引擎会自动将 PubDate DateTime 转换为字符串。但自动转换会同时显示日期和时间,而我们只想显示日期。若要修复此问题,我们可以创建自己的值转换器来将 DateTime 转换为字符串,并且可以在其中将字符串设置为任何需要的格式。

若要创建值转换器,则先创建一个用于实现 IValueConverter 接口的类,然后实现 Convert ConvertBack 方法。转换器可以将数据从一种类型更改为另一种类型,根据文化背景转换数据,或者修改数据呈现方式的其他方面。在这里,我们创建一个日期转换器,它将转换传入的日期值并设置其格式,从而只显示日、月和年。

注意  若要向项目添加新类,请选择“项目”>“添加类”。将类命名为 DateConverter(.cs 或 .vb)。

ConvertConvertBack 方法还允许你传入一个参数,以便通过不同的选项使用该转换器的同一个实例。在此示例中,我们包含了一个格式设置转换器,它可以根据输入的参数生成不同格式的日期。你可以使用 Binding 类的 ConverterParameterConvertConvertBack 方法中传递参数。稍后,我们将修改列表视图来演示上述内容。

具体代码如下
using System;
using Windows.Globalization.DateTimeFormatting;

namespace WindowsBlogReader
{
    public class DateConverter : Windows.UI.Xaml.Data.IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string culture)
        {
            if (value == null)
                throw new ArgumentNullException("value", "Value cannot be null.");

            if (!typeof(DateTime).Equals(value.GetType()))
                throw new ArgumentException("Value must be of type DateTime.", "value");
            
            DateTime dt = (DateTime)value;

            if (parameter == null)
            {
                // Date "7/27/2011 9:30:59 AM" returns "7/27/2011"
                return DateTimeFormatter.ShortDate.Format(dt);
            }           
            else if ((string)parameter == "day")
            {
                // Date "7/27/2011 9:30:59 AM" returns "27"
                DateTimeFormatter dateFormatter = new DateTimeFormatter("{day.integer(2)}");
                return dateFormatter.Format(dt);
            }
            else if ((string)parameter == "month")
            {
                // Date "7/27/2011 9:30:59 AM" returns "JUL"
                DateTimeFormatter dateFormatter = new DateTimeFormatter("{month.abbreviated(3)}");
                return dateFormatter.Format(dt).ToUpper();
            }
            else if ((string)parameter == "year")
            {
                // Date "7/27/2011 9:30:59 AM" returns "2011"
                DateTimeFormatter dateFormatter = new DateTimeFormatter("{year.full}");
                return dateFormatter.Format(dt);
            }
            else
            {
                // Requested format is unknown. Return in the original format.
                return dt.ToString();
            }
        }

        public object ConvertBack(object value, Type targetType, object parameter, string culture)
        {
            string strValue = value as string;
            DateTime resultDateTime;
            if (DateTime.TryParse(strValue, out resultDateTime))
            {
                return resultDateTime;
            }
            return Windows.UI.Xaml.DependencyProperty.UnsetValue;
        }
    }
}
 

使用 DateConverter 类之前,必须在我们的 XAML 中声明一个该类的实例。我们将关键字 dateConverter 作为 App.xaml 中应用资源来声明实例。在此处声明实例后,在应用的每个页面都可以使用它。

  <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                ...
                <ResourceDictionary>
                    <local:FeedDataSource x:Key="feedDataSource"/>
               
                    <!-- Add the DateConverter here. -->
                    <local:DateConverter x:Key="dateConverter" />

                </ResourceDictionary>

            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>


我们便可在绑定中使用 DateConverter 了。以下是来自 ItemListView DataTemplate 中更新后的 PubDate 绑定。

<TextBlock Text="{Binding Path=PubDate, Converter={StaticResource dateConverter}}" 
           FontSize="16" Margin="15,0,0,0"/>



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值