DirectShow学习之二做一个简单的媒体文件播放器

  3023人阅读  评论(3)  收藏  举报

作者:liguisen

 
       或许你现在还不知道directshow是什么东西,不过你应该知道它可以用来播放电影,ok,我们就做一个简单的媒体文件播放器。什么?我还不知道ds的原理呢,我不懂com呢,我甚至没有什么windows程序的经验呢。都没关系,我们先有一点成就感,再逐步深入。愣是够!
 
       首先,我们需要找两个叫做CDXGraph.h和CDXGraph.cpp的文件。在DX90SDK安装目录搜索了一遍,没有,在网上搜索一把,嗯,有很多,原来是一个“使用directShow播放mpeg的基类”,是“自定义的Filter Graph Manager的一个封装类”,(不知道Filter Graph Manager?没关系),作者不详,本文后面将列出。
 
第一步:(本文使用vs2005)建立一个名叫MyPlayer的MFC dialog based工程。
第二步:配置链接库,假设当前工程是debug版本(这都不清楚的话,嗯,还是慢慢来,先掌握基础好了),则打开project->properties->configuration properties->Linker ->Input->Additional Dependencies增加strmbasd.lib uuid.lib winmm.lib(以后不会说这么详细了)。
第三步:COM库的初始化和清除,直接列出代码了(把系统的注释去掉了)
BOOL CMyPlayerApp::InitInstance()
{
       INITCOMMONCONTROLSEX InitCtrls;
       InitCtrls.dwSize = sizeof(InitCtrls);
       InitCtrls.dwICC = ICC_WIN95_CLASSES;
       InitCommonControlsEx(&InitCtrls);
       CWinApp::InitInstance();
       AfxEnableControlContainer();      
       SetRegistryKey(_T("Local AppWizard-Generated Applications"));
 
       CoInitialize(NULL);//这是我们要增加的,com库的初始化
 
       CMyPlayerDlg dlg;
       m_pMainWnd = &dlg;
       INT_PTR nResponse = dlg.DoModal();
       if (nResponse == IDOK)
       {    
       }
       else if (nResponse == IDCANCEL)
       {
       }
       return FALSE;
}
//友情提示,下面这个函数系统并没有自动添加,在CMyPlayerApp类
//的properties里单击overrides可找到,发现很多用惯了vc6的“老程序员”
//没有了向导很不习惯呢。
int CMyPlayerApp::ExitInstance()
{
       CoUninitialize();//这是我们要增加的,清除com库
       return CWinApp::ExitInstance();
}
第四步:界面开发。
在对话框上放置一个Picture Control,(此类静态控件默认ID为IDC_STATIC,如果在程序中不对它们进行控制,不用修改ID,如果要控制,必须修改其ID号,才可对它进行代码控制,包括添加变量),修改ID为IDC_VIDEO_WINDOW,关联一个Control变量mVideoWindow。
添加一个“打开”和“播放”按钮。
 
第五步:添加CDXGraph.h和CDXGraph.cpp到工程。首先把这两个文件复制到工程源代码所在的地方,然后打开Project->Add Existing Item把它们真正加到工程。
 
第六步: MyPlayerDlg.h 增加include,如下:
#include "afxwin.h"
#include <streams.h>
#include "CDXGraph.h"
再增加两个成员变量:
CDXGraph *   mFilterGraph;     // Filter Graph封装
CString      mSourceFile;      // 源文件
并做一些初始化工作:
CMyPlayerDlg::CMyPlayerDlg(CWnd* pParent /*=NULL*/)
       : CDialog(CMyPlayerDlg::IDD, pParent)
{
       m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
       mFilterGraph = NULL;
       mSourceFile = "";
}
 
此时F7编译,如果出现问题,请参考第一篇 DirectShow学习之一在vs2005中配置directshow开发环境  
 
第七步:添加成员函数CreateGraph和DestroyGraph,编写代码如下:
void CMyPlayerDlg::CreateGraph(void)
{
       DestroyGraph();
       mFilterGraph = new CDXGraph();
       if (mFilterGraph->Create())
       {
              mFilterGraph->RenderFile(mSourceFile);
              mFilterGraph->SetDisplayWindow(mVideoWindow.GetSafeHwnd());
              mFilterGraph->SetNotifyWindow(this->GetSafeHwnd());
              mFilterGraph->Pause();
       }
}
 
void CMyPlayerDlg::DestroyGraph(void)
{
       if (mFilterGraph)
       {
              mFilterGraph->Stop();
              mFilterGraph->SetNotifyWindow(NULL);
              delete mFilterGraph;
              mFilterGraph = NULL;
       }
}
 
 
第八步:实现“打开”功能。
添加一个实现函数:
void CMyPlayerDlg::OnBnClickedButtonOpen()
{
       // TODO: Add your control notification handler code here
       CString    strFilter = "MPEG File (*.mpg;*.mpeg)|*.mpg;*.mpeg|";
       strFilter += "AVI File (*.avi)|*.avi|";
       strFilter += "Mp3 File (*.mp3)|*.mp3|";
       strFilter += "Wave File (*.wav)|*.wav|";
       strFilter += "All Files (*.*)|*.*|";
       CFileDialog dlgOpen(TRUE, NULL, NULL, OFN_PATHMUSTEXIST | OFN_HIDEREADONLY,
              strFilter, this);
       if (IDOK == dlgOpen.DoModal())
       {
              mSourceFile = dlgOpen.GetPathName();
              CreateGraph();
       }
}
 
此时,编译运行程序,打开一个mpeg文件,应该能显示第一帧的画面了。
第九步:实现“播放”功能。
添加一个实现函数:
void CMyPlayerDlg::OnBnClickedButtonPlay()
{
       // TODO: Add your control notification handler code here
       if (mFilterGraph)
       {
              mFilterGraph->Run();
       }
}
基本上,我们现在就可以用它来看电影了,呵呵。不过,还是有点小问题,当画面被其它窗口挡住之后,再重新出来,画面没了,不能更新,这是小问题:在OnInitDialog()里的return TRUE;前加一句mVideoWindow.ModifyStyle(0, WS_CLIPCHILDREN);然后添加主对话框的WM_ERASEBKGND消息响应:
BOOL CMyPlayerDlg::OnEraseBkgnd(CDC* pDC)
{
       // TODO: Add your message handler code here and/or call default
       CRect rc;
          mVideoWindow.GetWindowRect(&rc);
           ScreenToClient(&rc);
           pDC->ExcludeClipRect(&rc);
       return CDialog::OnEraseBkgnd(pDC);
}
 
尽管太过于简陋,但是,毕竟是一个播放器了,下一篇将详细分析程序的实现过程,重点是CDXGraph类。
 
 
附:CDXGraph.h和CDXGraph.cpp
//
// CDXGraph.h
//
 
#ifndef __H_CDXGraph__
#define __H_CDXGraph__
 
// Filter graph notification to the specified window
#define WM_GRAPHNOTIFY (WM_USER+20)
 
class CDXGraph
{
private:
       IGraphBuilder *     mGraph; 
       IMediaControl *            mMediaControl;
       IMediaEventEx *           mEvent;
       IBasicVideo *         mBasicVideo;
       IBasicAudio *        mBasicAudio;
       IVideoWindow *         mVideoWindow;
       IMediaSeeking *            mSeeking;
 
       DWORD                      mObjectTableEntry;
 
public:
       CDXGraph();
       virtual ~CDXGraph();
 
public:
       virtual bool Create(void);
       virtual void Release(void);
       virtual bool Attach(IGraphBuilder * inGraphBuilder);
 
       IGraphBuilder * GetGraph(void); // Not outstanding reference count
       IMediaEventEx * GetEventHandle(void);
 
       bool ConnectFilters(IPin * inOutputPin, IPin * inInputPin, const AM_MEDIA_TYPE * inMediaType = 0);
       void DisconnectFilters(IPin * inOutputPin);
 
       bool SetDisplayWindow(HWND inWindow);
       bool SetNotifyWindow(HWND inWindow);
       bool ResizeVideoWindow(long inLeft, long inTop, long inWidth, long inHeight);
       void HandleEvent(WPARAM inWParam, LPARAM inLParam);
 
       bool Run(void);        // Control filter graph
       bool Stop(void);
       bool Pause(void);
       bool IsRunning(void); // Filter graph status
       bool IsStopped(void);
       bool IsPaused(void);
 
       bool SetFullScreen(BOOL inEnabled);
       bool GetFullScreen(void);
 
       // IMediaSeeking
       bool GetCurrentPosition(double * outPosition);
       bool GetStopPosition(double * outPosition);
       bool SetCurrentPosition(double inPosition);
       bool SetStartStopPosition(double inStart, double inStop);
       bool GetDuration(double * outDuration);
       bool SetPlaybackRate(double inRate);
 
       // Attention: range from -10000 to 0, and 0 is FULL_VOLUME.
       bool SetAudioVolume(long inVolume);
       long GetAudioVolume(void);
       // Attention: range from -10000(left) to 10000(right), and 0 is both.
       bool SetAudioBalance(long inBalance);
       long GetAudioBalance(void);
 
       bool RenderFile(const char * inFile);
       bool SnapshotBitmap(const char * outFile);
 
private:
       void AddToObjectTable(void) ;
       void RemoveFromObjectTable(void);
      
       bool QueryInterfaces(void);
};
 
#endif // __H_CDXGraph__
 
 
 
 
//
// CDXGraph.cpp
//
 
#include "stdafx.h"
#include <streams.h>
#include "CDXGraph.h"
 
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
 
CDXGraph::CDXGraph()
{
       mGraph        = NULL;
       mMediaControl = NULL;
       mEvent        = NULL;
       mBasicVideo   = NULL;
       mBasicAudio   = NULL;
       mVideoWindow = NULL;
       mSeeking      = NULL;
 
       mObjectTableEntry = 0;
}
 
CDXGraph::~CDXGraph()
{
       Release();
}
 
bool CDXGraph::Create(void)
{
       if (!mGraph)
       {
              if (SUCCEEDED(CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
                     IID_IGraphBuilder, (void **)&mGraph)))
              {
                     AddToObjectTable();
 
                     return QueryInterfaces();
              }
              mGraph = 0;
       }
       return false;
}
 
bool CDXGraph::QueryInterfaces(void)
{
       if (mGraph)
       {
              HRESULT hr = NOERROR;
              hr |= mGraph->QueryInterface(IID_IMediaControl, (void **)&mMediaControl);
              hr |= mGraph->QueryInterface(IID_IMediaEventEx, (void **)&mEvent);
              hr |= mGraph->QueryInterface(IID_IBasicVideo, (void **)&mBasicVideo);
              hr |= mGraph->QueryInterface(IID_IBasicAudio, (void **)&mBasicAudio);
              hr |= mGraph->QueryInterface(IID_IVideoWindow, (void **)&mVideoWindow);
              hr |= mGraph->QueryInterface(IID_IMediaSeeking, (void **)&mSeeking);
              if (mSeeking)
              {
                     mSeeking->SetTimeFormat(&TIME_FORMAT_MEDIA_TIME);
              }
              return SUCCEEDED(hr);
       }
       return false;
}
 
void CDXGraph::Release(void)
{
       if (mSeeking)
       {
              mSeeking->Release();
              mSeeking = NULL;
       }
       if (mMediaControl)
       {
              mMediaControl->Release();
              mMediaControl = NULL;
       }
       if (mEvent)
       {
              mEvent->Release();
              mEvent = NULL;
       }
       if (mBasicVideo)
       {
              mBasicVideo->Release();
              mBasicVideo = NULL;
       }
       if (mBasicAudio)
       {
              mBasicAudio->Release();
              mBasicAudio = NULL;
       }
       if (mVideoWindow)
       {
              mVideoWindow->put_Visible(OAFALSE);
              mVideoWindow->put_MessageDrain((OAHWND)NULL);
              mVideoWindow->put_Owner(OAHWND(0));
              mVideoWindow->Release();
              mVideoWindow = NULL;
       }
       RemoveFromObjectTable();
       if (mGraph)
       {
              mGraph->Release();
              mGraph = NULL;
       }
}
 
bool CDXGraph::Attach(IGraphBuilder * inGraphBuilder)
{
       Release();
 
       if (inGraphBuilder)
       {
              inGraphBuilder->AddRef();
              mGraph = inGraphBuilder;
 
              AddToObjectTable();
              return QueryInterfaces();
       }
       return true;
}
 
IGraphBuilder * CDXGraph::GetGraph(void)
{
       return mGraph;
}
 
IMediaEventEx * CDXGraph::GetEventHandle(void)
{
       return mEvent;
}
 
// Connect filter from the upstream output pin to the downstream input pin
bool CDXGraph::ConnectFilters(IPin * inOutputPin, IPin * inInputPin,
                                                  const AM_MEDIA_TYPE * inMediaType)
{
       if (mGraph && inOutputPin && inInputPin)
       {
              HRESULT hr = mGraph->ConnectDirect(inOutputPin, inInputPin, inMediaType);
              return SUCCEEDED(hr) ? true : false;
       }
       return false;
}
 
void CDXGraph::DisconnectFilters(IPin * inOutputPin)
{
       if (mGraph && inOutputPin)
       {
              HRESULT hr = mGraph->Disconnect(inOutputPin);
       }
}
 
bool CDXGraph::SetDisplayWindow(HWND inWindow)
{    
       if (mVideoWindow)
       {
       //     long lVisible;
       //     mVideoWindow->get_Visible(&lVisible);
              // Hide the video window first
              mVideoWindow->put_Visible(OAFALSE);
              mVideoWindow->put_Owner((OAHWND)inWindow);
 
              RECT windowRect;
              ::GetClientRect(inWindow, &windowRect);
              mVideoWindow->put_Left(0);
              mVideoWindow->put_Top(0);
              mVideoWindow->put_Width(windowRect.right - windowRect.left);
              mVideoWindow->put_Height(windowRect.bottom - windowRect.top);
              mVideoWindow->put_WindowStyle(WS_CHILD|WS_CLIPCHILDREN|WS_CLIPSIBLINGS);
 
              mVideoWindow->put_MessageDrain((OAHWND) inWindow);
              // Restore the video window
              if (inWindow != NULL)
              {
              //     mVideoWindow->put_Visible(lVisible);
                     mVideoWindow->put_Visible(OATRUE);
              }
              else
              {
                     mVideoWindow->put_Visible(OAFALSE);
              }
              return true;
       }
       return false;
}
 
bool CDXGraph::ResizeVideoWindow(long inLeft, long inTop, long inWidth, long inHeight)
{
       if (mVideoWindow)
       {
              long lVisible = OATRUE;
              mVideoWindow->get_Visible(&lVisible);
              // Hide the video window first
              mVideoWindow->put_Visible(OAFALSE);
 
              mVideoWindow->put_Left(inLeft);
              mVideoWindow->put_Top(inTop);
              mVideoWindow->put_Width(inWidth);
              mVideoWindow->put_Height(inHeight);
      
              // Restore the video window
              mVideoWindow->put_Visible(lVisible);
              return true;
       }
       return false;
}
 
bool CDXGraph::SetNotifyWindow(HWND inWindow)
{
       if (mEvent)
       {
              mEvent->SetNotifyWindow((OAHWND)inWindow, WM_GRAPHNOTIFY, 0);
              return true;
       }
       return false;
}
 
void CDXGraph::HandleEvent(WPARAM inWParam, LPARAM inLParam)
{
       if (mEvent)
       {
              LONG eventCode = 0, eventParam1 = 0, eventParam2 = 0;
              while (SUCCEEDED(mEvent->GetEvent(&eventCode, &eventParam1, &eventParam2, 0)))
              {
                     mEvent->FreeEventParams(eventCode, eventParam1, eventParam2);
                     switch (eventCode)
                     {
                     case EC_COMPLETE:
                            break;
 
                     case EC_USERABORT:
                     case EC_ERRORABORT:
                            break;
 
                     default:
                            break;
                     }
              }
       }
}
 
bool CDXGraph::Run(void)
{
       if (mGraph && mMediaControl)
       {
              if (!IsRunning())
              {
                     if (SUCCEEDED(mMediaControl->Run()))
                     {
                            return true;
                     }
              }
              else
              {
                     return true;
              }
       }
       return false;
}
 
bool CDXGraph::Stop(void)
{
       if (mGraph && mMediaControl)
       {
              if (!IsStopped())
              {    
                     if (SUCCEEDED(mMediaControl->Stop()))
                     {
                            return true;
                     }
              }
              else
              {
                     return true;
              }
       }
       return false;
}
 
bool CDXGraph::Pause(void)
{
       if (mGraph && mMediaControl)
       {
              if (!IsPaused())
              {    
                     if (SUCCEEDED(mMediaControl->Pause()))
                     {
                            return true;
                     }
              }
              else
              {
                     return true;
              }
       }
       return false;
}
 
bool CDXGraph::IsRunning(void)
{
       if (mGraph && mMediaControl)
       {
              OAFilterState state = State_Stopped;
              if (SUCCEEDED(mMediaControl->GetState(10, &state)))
              {
                     return state == State_Running;
              }
       }
       return false;
}
 
bool CDXGraph::IsStopped(void)
{
       if (mGraph && mMediaControl)
       {
              OAFilterState state = State_Stopped;
              if (SUCCEEDED(mMediaControl->GetState(10, &state)))
              {
                     return state == State_Stopped;
              }
       }
       return false;
}
 
bool CDXGraph::IsPaused(void)
{
       if (mGraph && mMediaControl)
       {
              OAFilterState state = State_Stopped;
              if (SUCCEEDED(mMediaControl->GetState(10, &state)))
              {
                     return state == State_Paused;
              }
       }
       return false;
}
 
bool CDXGraph::SetFullScreen(BOOL inEnabled)
{
       if (mVideoWindow)
       {
              HRESULT hr = mVideoWindow->put_FullScreenMode(inEnabled ? OATRUE : OAFALSE);
              return SUCCEEDED(hr);
       }
       return false;
}
 
bool CDXGraph::GetFullScreen(void)
{
       if (mVideoWindow)
       {
              long fullScreenMode = OAFALSE;
              mVideoWindow->get_FullScreenMode(&fullScreenMode);
              return (fullScreenMode == OATRUE);
       }
       return false;
}
 
// IMediaSeeking features
bool CDXGraph::GetCurrentPosition(double * outPosition)
{
       if (mSeeking)
       {
              __int64 position = 0;
              if (SUCCEEDED(mSeeking->GetCurrentPosition(&position)))
              {
                     *outPosition = ((double)position) / 10000000.;
                     return true;
              }
       }
       return false;
}
 
bool CDXGraph::GetStopPosition(double * outPosition)
{
       if (mSeeking)
       {
              __int64 position = 0;
              if (SUCCEEDED(mSeeking->GetStopPosition(&position)))
              {
                     *outPosition = ((double)position) / 10000000.;
                     return true;
              }
       }
       return false;
}
 
bool CDXGraph::SetCurrentPosition(double inPosition)
{
       if (mSeeking)
       {
              __int64 one = 10000000;
              __int64 position = (__int64)(one * inPosition);
              HRESULT hr = mSeeking->SetPositions(&position, AM_SEEKING_AbsolutePositioning | AM_SEEKING_SeekToKeyFrame,
                     0, AM_SEEKING_NoPositioning);
              return SUCCEEDED(hr);
       }
       return false;
}
 
bool CDXGraph::SetStartStopPosition(double inStart, double inStop)
{
       if (mSeeking)
       {
              __int64 one = 10000000;
              __int64 startPos = (__int64)(one * inStart);
              __int64 stopPos = (__int64)(one * inStop);
              HRESULT hr = mSeeking->SetPositions(&startPos, AM_SEEKING_AbsolutePositioning | AM_SEEKING_SeekToKeyFrame,
                     &stopPos, AM_SEEKING_AbsolutePositioning | AM_SEEKING_SeekToKeyFrame);
              return SUCCEEDED(hr);
       }
       return false;
}
 
bool CDXGraph::GetDuration(double * outDuration)
{
       if (mSeeking)
       {
              __int64 length = 0;
              if (SUCCEEDED(mSeeking->GetDuration(&length)))
              {
                     *outDuration = ((double)length) / 10000000.;
                     return true;
              }
       }
       return false;
}
 
bool CDXGraph::SetPlaybackRate(double inRate)
{
       if (mSeeking)
       {
              if (SUCCEEDED(mSeeking->SetRate(inRate)))
              {
                     return true;
              }
       }
       return false;
}
 
// Attention: range from -10000 to 0, and 0 is FULL_VOLUME.
bool CDXGraph::SetAudioVolume(long inVolume)
{
       if (mBasicAudio)
       {
              HRESULT hr = mBasicAudio->put_Volume(inVolume);
              return SUCCEEDED(hr);
       }
       return false;
}
 
long CDXGraph::GetAudioVolume(void)
{
       long volume = 0;
       if (mBasicAudio)
       {
              mBasicAudio->get_Volume(&volume);
       }
       return volume;
}
 
// Attention: range from -10000(left) to 10000(right), and 0 is both.
bool CDXGraph::SetAudioBalance(long inBalance)
{
       if (mBasicAudio)
       {
              HRESULT hr = mBasicAudio->put_Balance(inBalance);
              return SUCCEEDED(hr);
       }
       return false;
}
 
long CDXGraph::GetAudioBalance(void)
{
       long balance = 0;
       if (mBasicAudio)
       {
              mBasicAudio->get_Balance(&balance);
       }
       return balance;
}
 
bool CDXGraph::RenderFile(const char * inFile)
{
       if (mGraph)
       {
              WCHAR    szFilePath[MAX_PATH];
              MultiByteToWideChar(CP_ACP, 0, inFile, -1, szFilePath, MAX_PATH);
              if (SUCCEEDED(mGraph->RenderFile(szFilePath, NULL)))
              {
                     return true;
              }
       }
       return false;
}
 
bool CDXGraph::SnapshotBitmap(const char * outFile)
{
       if (mBasicVideo)
       {
              long bitmapSize = 0;
              if (SUCCEEDED(mBasicVideo->GetCurrentImage(&bitmapSize, 0)))
              {
                     bool pass = false;
                     unsigned char * buffer = new unsigned char[bitmapSize];
                     if (SUCCEEDED(mBasicVideo->GetCurrentImage(&bitmapSize, (long *)buffer)))
                     {
                            BITMAPFILEHEADER hdr;
                            LPBITMAPINFOHEADER    lpbi;
 
                            lpbi = (LPBITMAPINFOHEADER)buffer;
 
                            int nColors = 1 << lpbi->biBitCount;
                            if (nColors > 256)
                                   nColors = 0;
 
                            hdr.bfType             = ((WORD) ('M' << 8) | 'B'); //always is "BM"
                            hdr.bfSize              = bitmapSize + sizeof( hdr );
                            hdr.bfReserved1    = 0;
                            hdr.bfReserved2    = 0;
                            hdr.bfOffBits         = (DWORD) (sizeof(BITMAPFILEHEADER) + lpbi->biSize +
                                          nColors * sizeof(RGBQUAD));
 
                            CFile bitmapFile(outFile, CFile::modeReadWrite | CFile::modeCreate | CFile::typeBinary);
                            bitmapFile.Write(&hdr, sizeof(BITMAPFILEHEADER));
                            bitmapFile.Write(buffer, bitmapSize);
                            bitmapFile.Close();
                            pass = true;
                     }
                     delete [] buffer;
                     return pass;
              }
       }
       return false;
}
 
 
 
For GraphEdit Dubug purpose /
void CDXGraph::AddToObjectTable(void)
{
       IMoniker * pMoniker = 0;
    IRunningObjectTable * objectTable = 0;
    if (SUCCEEDED(GetRunningObjectTable(0, &objectTable)))
       {
              WCHAR wsz[256];
              wsprintfW(wsz, L"FilterGraph %08p pid %08x", (DWORD_PTR)mGraph, GetCurrentProcessId());
              HRESULT hr = CreateItemMoniker(L"!", wsz, &pMoniker);
              if (SUCCEEDED(hr))
              {
                     hr = objectTable->Register(0, mGraph, pMoniker, &mObjectTableEntry);
                     pMoniker->Release();
              }
              objectTable->Release();
       }
}
 
void CDXGraph::RemoveFromObjectTable(void)
{
       IRunningObjectTable * objectTable = 0;
    if (SUCCEEDED(GetRunningObjectTable(0, &objectTable)))
       {
        objectTable->Revoke(mObjectTableEntry);
        objectTable->Release();
              mObjectTableEntry = 0;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值