类似vista的任务对话框

下载sample project and source code - 171 KB

介绍

你们有些人可能知道,未来版本的Windows,代号为Vista将包括更丰富的用户体验。作为的一部分,Windows Vista将支持一种新的对话框命名任务对话框。

任务对话框非常类似于普通消息框,但他们支持更多的选项,通常更灵活。任务对话框,我们可以实现非常简单但富有对话框用于提示,以及更复杂的类似于向导的应用程序。我理解,许多操作在Windows Vista将面临的任务序列对话框。

一个任务对话框下面的一般结构:

窗口标题(或者标题)主要指令,显示一个图标和一个texta内容区域显示一个描述和支持hyperlinksa组无线电buttonsa进步巴拉组自定义按钮,以及一些常见的buttonsa复选框,允许简单的场景像“不再显示此对话框”一个脚注区,显示一个小图标和文本,支持超链接

本文介绍了我这个新功能的实现,设计源代码兼容与即将到来的API。实际上,因为这个功能只可在Windows Vista并不意味着你不一定要今天在您的应用程序中使用它。所以我决定试着自己实现这个。

这个定制实现,你也可以使用任务对话框应用程序设计为在Windows 2000和后来的平台上工作。

如何在您的项目中使用它

本文的下载功能的动态库commctrl_taskdialogs.dll暴露组成任务对话框的函数API。它还包含一个头文件,commctrl_taskdialogs。h,包括在你的应用程序访问的声明类型,任务对话框的结构和功能的API。动态库使用ATL和WTL实现,因此没有外部依赖。

在Windows Vista,这个API将由comctl32.dll暴露并通过commctrl.h头文件。因为这个实现是设计为源代码兼容,您应该能够取代包含指令当Vista最终船只。

如果你读的描述任务对话框API,你会发现很容易实现一个简单的提示,如上面所示的图。下面的代码演示如何显示这样一个对话框:

隐藏,复制Code #include <commctrl_taskdialogs.h>
// on Vista, use #include <commctrl.h> directly

int button;

::TaskDialog(
      ::GetActiveWindow()                // parent window handle
    , ::AfxGetResourceHandle()            // resource handle
    , MAKEINTRESOURCE(IDR_MAINFRAME)        // window title
    , MAKEINTRESOURCE(IDS_TASK_WELCOME_INSTRUCTION)    // main instruction
    , MAKEINTRESOURCE(IDS_TASK_WELCOME_CONTENT)    // text in the content area
    , TDCBF_YES_BUTTON | TDCBF_NO_BUTTON        // common buttons
    , MAKEINTRESOURCE(IDR_MAINFRAME)        // icon
    , &button
    );

if (button == IDYES) {

    // creates and displays the first task dialog
    // that will take part in the navigation sequence

    ...
}

)

很简单,不是吗?尽管是非常灵活的,任务对话框API暴露只有两个函数。这是正确的。

TaskDialog函数本身相当于对话框API。它不做太多,除了允许的任何组合按钮通常在一个消息框。不过,一个显著特点是任务对话框的可能性从资源加载字符串和图标,使用指定的资源处理结合MAKEINTRESOURCE宏。

这个API的真正威力在于TaskDialogIndirect函数,您可能猜到,TASKDIALOGCONFIG结构+其他参数。这种结构的成员组织的总体结构任务对话框,如上图所示。是什么使它强大的可能性为应用程序指定一个回调函数,有趣的事情发生了在任务时将调用对话框中,如当一个按钮被按下时,当一个单选按钮或复选框被选中时,当点击超链接时,等等。如您所见,这个函数比MessageBoxIndirect方式更强大的API。事实上,加上其内置的支持一个进度条和回调计时器,和从一个任务对话框导航到另一个可能性,TaskDialogIndirect功能很像一个迷你应用程序框架。

TASKDIALOGCONFIG结构包含了无数成员,它可能需要一段时间适应。幸运的是,很容易创建一些包装它,所以你会发现包装器类为ATL和MFC项目在本文的下载。

包装器类

因为它可能是乏味的使用原始TaskDialogIndirect函数和维护直接TASKDIALOGCONFIG结构的所有成员,我提供了一些包装类,您可以使用在你的MFC和ATL项目。使用这些包装类绝不是强制性的,然而,我在头文件包含一组宏使用任务对话框支持从一个普通的Win32 API SDK应用程序。

包装器类使其易于管理和操作任务对话框,和包括支持回调通知。它包含一组虚函数,称为自动的框架,和一组方法,管理信息的传播任务对话框。

例如,任务对话框显示在本文的开始是由下面的代码样例应用程序中实现:

隐藏,收缩,复制鳕鱼eclass CCheckBoxTaskDialog : public CTaskDialog
{
// construction / destruction
public:

CCheckBoxTaskDialog()
    : CTaskDialog(
          MAKEINTRESOURCE(IDS_TASK_CHECK_BOX_INSTRUCTION)
        , MAKEINTRESOURCE(IDS_TASK_CHECK_BOX_CONTENT)
        , MAKEINTRESOURCE(IDR_MAINFRAME)
        , 0
        , MAKEINTRESOURCE(IDR_MAINFRAME))
{

    // only allow hyperlinks on systems that support it
    // i.e. check for the version of the common control library

    if (DllGetVersion(L"comctl32") >= MAKEDLLVERULL(6, 0, 0, 0)) {
        SetFlags(TDF_ENABLE_HYPERLINKS);
        SetVerificationText(MAKEINTRESOURCE(
           IDS_TASK_CHECK_BOX_CHECK_ENABLE_HYPERLINKS));
    }

    SetFlags(TDF_ALLOW_DIALOG_CANCELLATION);
    SetCommonButtons(TDCBF_CLOSE_BUTTON);
    AddButton(IDC_TASK_CHECK_BOX_BUTTON_CONTINUE);

    SetFooter(MAKEINTRESOURCE(IDS_TASK_CHECK_BOX_FOOTER));
    SetFooterIcon(MAKEINTRESOURCE(IDR_MAINFRAME));
}

// overrides
private:

BOOL OnButtonClicked(UINT uID)
{
    if (uID == IDC_TASK_CHECK_BOX_BUTTON_CONTINUE) {
        Navigate(ProgressBarTaskDialog_);
        return TRUE;
    }
    return FALSE;
}

void OnVerificationClicked(BOOL bChecked)
{
    if (bChecked) {
        SetContent(MAKEINTRESOURCE(IDS_TASK_CHECK_BOX_CONTENT_HYPERLINKS));
        SetFooter(MAKEINTRESOURCE(IDS_TASK_CHECK_BOX_FOOTER_HYPERLINKS));
    }

    else {
        SetContent(MAKEINTRESOURCE(IDS_TASK_CHECK_BOX_CONTENT));
        SetFooter(MAKEINTRESOURCE(IDS_TASK_CHECK_BOX_FOOTER));
    }
}

void OnHyperLinkClicked(LPCWSTR wszHREF)
{
    ::ShellExecuteW(GetSafeHwnd(), L"open", wszHREF, L"", L"", SW_SHOWNORMAL);
}

// data members
private:

CProgressBarTaskDialog ProgressBarTaskDialog_;

};

作为参考,这里是CTaskDialog MFC包装类的声明。ATL包装类非常相似,包含相同的方法和虚函数:

隐藏,收缩,复制Code#ifndef _INC_AFXTASK
#define _INC_AFXTASK

class CSimpleTaskDialog : public CWnd
{
// construction / destruction
public:

DECLARE_DYNAMIC(CSimpleTaskDialog)

CSimpleTaskDialog(
      LPCTSTR instruction = _T("")
    , LPCTSTR content = _T("")
    , LPCTSTR title = _T("")
    , TASKDIALOG_COMMON_BUTTON_FLAGS buttons = TDCBF_OK_BUTTON
    , LPCTSTR icon = 0);

virtual ~CSimpleTaskDialog();

// attributes
public:

virtual int GetButton(void) const;

virtual void SetWindowTitle(LPCTSTR title);
virtual void SetMainInstruction(LPCTSTR instruction);
virtual void SetContent(LPCTSTR content);
virtual void SetIcon(LPCTSTR icon);

// operations
public:

virtual INT_PTR DoModal(CWnd* pParentWnd = CWnd::GetActiveWindow());

// data members
protected:

TASKDIALOGCONFIG m_config;
int m_button;

};

class CTaskDialog : public CSimpleTaskDialog
{
// construction / destruction
public:

DECLARE_DYNAMIC(CTaskDialog)

CTaskDialog(
          LPCTSTR instruction = _T("")
        , LPCTSTR content = _T("")
        , LPCTSTR title = _T("")
        , TASKDIALOG_COMMON_BUTTON_FLAGS buttons = TDCBF_OK_BUTTON
        , LPCTSTR icon = 0);

// attributes
public:

int GetRadioButton(void) const;
BOOL IsVerificationChecked(void) const;

void SetFlags(TASKDIALOG_FLAGS dwFlags, TASKDIALOG_FLAGS dwExMask);

void SetCommonButtons(TASKDIALOG_COMMON_BUTTON_FLAGS buttons);

void SetWindowTitle(LPCTSTR title);
void SetMainInstruction(LPCTSTR instruction);
void SetContent(LPCTSTR content);

void SetIcon(LPCTSTR icon);
void SetIcon(HICON hIcon);

void AddButton(UINT nButtonID, LPCTSTR pszButtonText = 0);
void AddRadioButton(UINT nButtonID, LPCTSTR pszButtonText = 0);

void SetDefaultButton(UINT uID);
void SetDefaultRadioButton(UINT uID);

void SetVerificationText(LPCTSTR verification, BOOL bChecked = FALSE);

void SetFooterIcon(HICON icon);
void SetFooterIcon(LPCTSTR pszIcon);

void SetFooter(LPCTSTR content);

// operations
public:

INT_PTR DoModal(CWnd* pParentWnd = CWnd::GetActiveWindow());

void ClickButton(UINT uID);
void ClickRadioButton(UINT uID);
void ClickVerification(BOOL bState, BOOL bFocus = FALSE);

void EnableButton(UINT uID, BOOL bEnabled = TRUE);
void EnableRadioButton(UINT uID, BOOL bEnabled = TRUE);

void SetProgressBarMarquee(BOOL bMarquee, UINT nSpeed);
void SetProgressBarPosition(UINT nPos);
void SetProgressBarRange(UINT nMinRange, UINT nMaxRange);
void SetProgressBarState(UINT nState);

BOOL UpdateElementText(TASKDIALOG_ELEMENTS te, LPCTSTR string);

void UpdateIcon(TASKDIALOG_ICON_ELEMENTS tie, LPCTSTR icon);
void UpdateIcon(TASKDIALOG_ICON_ELEMENTS tie, HICON hIcon);

void Navigate(const TASKDIALOGCONFIG* task_dialog);
void Navigate(const CTaskDialog& task_dialog);

// overrides
public:

virtual void OnDialogConstructed(void);
virtual void OnCreated(void);
virtual void OnDestroyed(void);
virtual void OnRadioButtonClicked(UINT uID);
virtual BOOL OnButtonClicked(UINT uID);
virtual void OnVerificationClicked(BOOL bChecked);
virtual void OnHyperLinkClicked(LPCWSTR wszHREF);
virtual void OnHelp(void);
virtual BOOL OnTimer(DWORD dwTickCount);
virtual void OnNavigated(void);

// implementation
private:

static HRESULT __stdcall TaskDialogCallbackProc(HWND hWnd, 
       UINT uCode, WPARAM wParam, LPARAM lParam, LONG_PTR data);

// data members
private:

int m_radiobutton;
BOOL m_verification;

CArray<TASKDIALOG_BUTTON> m_buttons;
CArray<TASKDIALOG_BUTTON> m_radioButtons;

};

#endif // _INC_AFXTASK

#include “afxtask.inl”

的兴趣点

编写本文附带的代码非常有趣,也很有挑战性。我将尝试在这里介绍开发过程中发生的各种有趣的点:

最初,我用内存中的对话框模板创建任务对话框。不幸的是,我不能使它可靠地工作,而且它被证明不如在空对话框上动态创建控件灵活。无论如何,这种方法对于支持任务对话框的导航特性是必要的,因为在这种情况下,窗口句柄会在主任务对话框和被导航到的任务对话框之间回收。

因此,在运行时创建控件更加棘手,因为与使用资源编辑器设计对话框相比,您需要注意更多的事情。您必须负责设置适当的字体,以z顺序(用于tabstop顺序)定位控件,并计算它将在对话框中占用的空间。算法我使用的位置控制看起来像这样:

计算文本区段

首先,我计算对话框的最大宽度,包括一些边距。我假设主指令和按钮行应该分别显示在一行上。因此,对于初学者,我认为对话框的宽度是这些值与TASKDIALOGCONFIG结构的一个成员中指定的宽度之间的最大值。但是,我想让内容区域看起来也不错,所以我尝试在内容区域的文本执行16:9的高宽比。我通过增加对话框的宽度来达到这个比例。

一个棘手的部分是计算内容区域中文本所占的矩形。据我所知,没有执行这些计算的内置函数,尽管有一些函数可以获得单行上的文本区段和符合指定宽度的字符数。因此,我需要自己执行计算,即在单词换行边界处分割文本的每一行,并在此过程中累积行高。

好吧,事实证明要比这复杂得多,因为我想处理换行字符(\n),但Windows认为换行字符根本不占用任何空间!这里是我使用的函数:

隐藏,收缩,复制Code/// implements GetTextExtentExPoint with additionnal support for newline-characters
static BOOL WINAPI GetTextExtentExPointExW(HDC hDC, LPCWSTR szText
, int cchString, int nMaxExtent
, LPINT lpnFit, LPINT alpDx, LPSIZE lpSize)
{

// first, check whether there is a newline character.
// if there is one, update the character count

LPWSTR _szNewLine = ::StrChr(szText, L'\n');
if (_szNewLine != 0) {
    int cch = 0;
    const WCHAR* _szText = szText;
    while (_szText <= _szNewLine) {
        _szText = ::CharNextW(_szText);
        ++cch;
    }

    cchString = min(cchString, cch);
}

// call the real function
BOOL bResult = ::GetTextExtentExPoint(hDC, szText, cchString, 
                 nMaxExtent, lpnFit, alpDx, lpSize);

// a single newline-character should take up
// the same space as one line would

if (_szNewLine != 0 && lpSize->cy == 0) {
    SIZE size;
    ::GetTextExtentExPoint(hDC, L"|", 1, 65536, 0, 0, &size);
    lpSize->cy = size.cy;
}

return bResult;

}

前面的函数返回适合单行的正确字符数,并考虑了可能的换行字符。同时,以换行字符开始的行会被认为占用一些空间,用一个|字符来模拟。

现在,我们可以计算给定文本的文本范围,考虑到单词的换行位置。我使用以下函数:

隐藏,收缩,复制Code/// implements GetTextExtent with additionnal support for word wrapped text
static BOOL WINAPI GetTextExtentExW(HDC hDC, LPCWSTR szText,
int cchString, int nMaxExtent, LPSIZE lpSize)
{
lpSize->cx = 0;
lpSize->cy = 0;

int nFit = 0;
SIZE extent = { 0, 0 };
const WCHAR* _szText = szText;

// calculate the number of characters that fit on a single line
// taking into account potential newline characters

if (::GetTextExtentExPointExW(hDC, _szText, cchString, 
                              nMaxExtent, &nFit, 0, &extent)) {

    // update the horizontal extent of the text

    lpSize->cx = min(nMaxExtent, extent.cx);

    // if the specified text fits on a single line,
    // update the vertical extent of the text

    if (nFit == cchString)
        lpSize->cy = extent.cy;

    // otherwise, break up each individual line and
    // accumulate the vertical dimensions

    else {

        _szText = GetWordWrapBoundary(_szText, &nFit);

        while (true) {

            // update the horizontal and vertical extents of the text

            lpSize->cx = min(nMaxExtent, max(lpSize->cx, extent.cx));
            lpSize->cy += extent.cy;

            if ((cchString -= nFit) == 0)
                break;

            // perform the calculation for the next line
            // and update the number of characters to consider

            ::GetTextExtentExPointExW(hDC, _szText, cchString, 
                                      nMaxExtent, &nFit, 0, &extent);
            _szText = GetWordWrapBoundary(_szText, &nFit);

        }
    }

    return TRUE;
}

return FALSE;

}

前面的函数使用了一个小实用函数GetWordWrapBoundary,该函数计算单词换行边界中最后一个字符的索引。使用该函数是为了避免使用[]下标操作符,并避免对字符是否具有固定大小做出任何假设。我不知道它是否与UNICODE字符串具有复杂的脚本,但我最好是安全比抱歉:

隐藏,收缩,复制Codestatic LPCWSTR WINAPI GetWordWrapBoundary(LPCWSTR szText, int* pAt)
{
// first go the the last specified character

const WCHAR* _szAt = szText;
for (int nAt = *pAt; nAt > 0; nAt--) {
    _szAt = ::CharNextW(_szAt);
    if (*_szAt == L'\0')
        break;
}

if (*_szAt == L'\0')
    return _szAt;

// then go back until we find a space or a newline character,
// updating the specified index as we go
const WCHAR* _szText = _szAt;

while (*_szText != L' ' && *_szText != L'\n' && _szText != szText) {
    _szText = ::CharPrevW(szText, _szText);
    --*pAt;
}

// finally, if we found a space or a newline character,
// go forward one character

if (_szText != _szAt) {
    _szText = ::CharNextW(_szText);
    ++*pAt;
}

return _szText;

}

基于对话框的宽度,我现在可以一次从上到下放置一个控件。首先,定位主图标和主指令静态控件,然后定位内容静态控件、单选按钮和进度条。在这个阶段,我知道了内容区域的最大高度,所以我将白色矩形框和蚀刻水平线放置在正确的位置。然后,按钮行、复选框和脚注区域控件都可以放置在对话框的底部。

取消任务对话框

另一个有趣的特性是处理任务对话框tdf_allow_dialog_cancel标志。如果指定了此标志,可以使用右上角的关闭按钮关闭任务对话框,或者使用ESC键或ALT+F4键组合。但是,如果未指定此标志,则无法以这种方式关闭任务对话框。另外,如果任务对话框中有一个通用的“取消”按钮,则会暗示此标志的设置。

带有系统菜单的标准对话框的行为如下:

点击关闭按钮发送一个代码为SC_CLOSE的WM_SYSMESSAGE消息,点击ALT+F4组合键执行相同的效果,即。,一个带有SC_CLOSEhitting ESC键的WM_SYSMESSAGE消息模拟了点击“取消”,即。,一个带有BN_CLICKED和ID IDCANCEL的WM_COMMAND消息

这些观察导致了一个有趣的实现。首先,我们需要启用或禁用系统关闭菜单基于tdf_allow_dialog_cancel标志的设置。下面的代码负责这部分:

隐藏,收缩,复制Codevoid CTaskDialog::EnableSystemClose(BOOL bEnabled)
{
// to enable the system close command, we restore
// the dialog box’ system menu (bEnabled == TRUE)

// otherwize, we create a copy of the system menu
// so that we can later modify it (bEnabled == FALSE)

HMENU hMenu = ::GetSystemMenu(m_hWnd, bEnabled);

if (bEnabled)
    return ;

if (hMenu != 0) {

    // first, lookup the index of the SC_CLOSE command

    int count = ::GetMenuItemCount(hMenu);
    for (int index = 0; index < count; index++) {
        DWORD dwID = ::GetMenuItemID(hMenu, index);
        if (::GetMenuItemID(hMenu, index) == SC_CLOSE)
            break;
    }

    if (index < count) {

        // remove the SC_CLOSE command

        ::RemoveMenu(hMenu, index, MF_BYPOSITION);

        // if the previous command is a separator
        // remove it as well to obtain a nice menu

        {
            MENUITEMINFO mnuItemInfo = { sizeof(MENUITEMINFO), MIIM_FTYPE };
            ::GetMenuItemInfo(hMenu, index - 1, TRUE, &mnuItemInfo);
            if (mnuItemInfo.fType == MFT_SEPARATOR)
                ::RemoveMenu(hMenu, index - 1, MF_BYPOSITION);
        }
    }
}

}

仅仅禁用系统关闭命令是不够的。正如我们所说的,对话管理器将处理ALT+F4组合键,并将此命令发送到我们的对话框。一个简单的方法是过滤掉系统关闭命令并委托给OnCancel()的处理程序:

隐藏,复制Codevoid CTaskDialog::OnSysCommand(UINT nCode, CPoint /* pt */)
{
SetMsgHandled(FALSE);
if ((nCode & 0xFFF0) == SC_CLOSE) {
SetMsgHandled(TRUE);
if (HasFlag(TDF_ALLOW_DIALOG_CANCELLATION))
OnCancel(BN_CLICKED, IDCANCEL, m_hWnd);
}
}

我们还需要过滤ESC键,因为它会被对话管理器转换为单击“取消”按钮。在我们的函数中,我们委托给一个按钮处理程序,因为回调函数可以防止任务对话框实际上关闭:

隐藏,复制CodeLRESULT CTaskDialog::OnCancel(UINT /* nCode /, int / nID /, HWND / hWnd */)
{
if ((::GetKeyState(VK_ESCAPE) & 0x80000) == 0 ||
HasFlag(TDF_ALLOW_DIALOG_CANCELLATION))
return OnButtonClicked(IDCANCEL);
return 0L;
}

最后一个改变是,回调函数可能启用或禁用任务对话框中的特定按钮,包括“取消按钮”。当“取消”按钮被禁用时,我们不想允许对话框取消。下面的代码片段用于处理这种情况:

隐藏,复制Codevoid CTaskDialog::OnEnableButton(UINT uID, BOOL bEnabled)
{
if (!::IsWindow(GetDlgItem(uID)))
return ;

::EnableWindow(GetDlgItem(uID), bEnabled);

if (uID == IDCANCEL) {
    if (bEnabled)
        config_.dwFlags |= TDF_ALLOW_DIALOG_CANCELLATION;
    else
        config_.dwFlags &= ~TDF_ALLOW_DIALOG_CANCELLATION;
    EnableSystemClose(HasFlag(TDF_ALLOW_DIALOG_CANCELLATION));
}

}

注意,我们实际上更改了tdf_allow_dialog_cancel标志的设置。但这是可以的,因为只有在任务对话框中有“取消”按钮时才会发生,在这种情况下,正如我们所说的,无论如何标志的设置都是隐含的。

限制

在现实生活中,任务对话框支持一个扩展区域,用于存放更多细节,这些细节只在用户请求时才显示。我的实现不包括这个特性。

同样,任务对话框将在Windows Vista中可用,并将使用升级后的通用控件,如命令链接。我没有在自定义实现中模拟此行为。

似乎在Windows Vista中,任务对话框API只能在UNICODE中使用。我的实现可以作为ANSI和UNICODE函数使用。但是,在处理TDN_HYPERLINK_CLICKED通知时有一个限制。在Windows中,超链接是通过只在UNICODE中可用的SysLink公共控件实现的。因此,这个通知将总是返回一个UNICODE字符串,即使对于ANSI任务对话框也是如此。

已知的问题

在脚注区域显示一个小的标准图标时出现问题。尽管我使用的::LoadImage API为小图标设置了合适的大小,但在我的系统(Windows XP)上,标准图标总是以默认的32x32像素加载。尽管如此,自定义图标还是可以工作的。

我使用一个自定义函数在不同时刻将焦点移动到任务对话框中的特定控件上。我应该为此使用标准的WM_NEXTDLGCTL消息,但它似乎没有为这个项目工作。也许这与对话框上的控件是动态创建的这一事实有关。无论如何,不能使这个工作,否则。

学分

虽然我主要使用了MSDN中任务对话框API的初步描述,但我也想提一下我在此过程中发现有用的文章:

首先,我是陈raymond的博客The Old New Thing的忠实读者。这篇文章开始了这一切……

我广泛地使用了Kenny Kerr的这篇文章,作为他的Windows Vista for Developers系列文章的一部分,描述了API和ATL c++包装器类的实现。

结论

本文介绍了即将到来的Windows Vista任务对话框API的自定义实现,并为ATL/WTL和MFC项目提供了包装类。

这个实现编写起来很令人兴奋,并且在许多方面都具有挑战性,我希望您喜欢阅读它,就像我喜欢创建这个项目一样。

我希望有人发现这有用。

本文转载于:http://www.diyabc.com/frontweb/news3645.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值