zoukankan      html  css  js  c++  java
  • 使用libcurl开源库和Duilib做的下载文件并显示进度条的小工具

    转载:http://blog.csdn.net/mfcing/article/details/43603525

    转载:http://blog.csdn.net/infoworld/article/details/46646933

    转载:http://blog.csdn.net/qq_25867649/article/details/52789467?locationNum=2

    转载:http://www.cnblogs.com/wing-h/p/3263488.html

    转载:http://blog.csdn.net/infoworld/article/details/46646933

    转载:http://blog.csdn.net/xiaojun111111/article/details/53032126

    转载:http://ysir.me/2015/08/05/libcurl%E5%AE%9E%E7%8E%B0%E6%96%AD%E7%82%B9%E7%BB%AD%E4%BC%A0/

    转载:http://blog.csdn.net/qq_25867649/article/details/52913501

    界面借用了网友mfcing,然后进行了修改

    每个下载任务包含的功能:

    1.任务名字

    2.下载剩余时间

    3.下载速度

    4.下载进度百分比

    5.继续下载

    6.暂停下载

    7.取消下载任务

    一、首先扩展Duilib下载List

    .h文件

    #pragma once
    
    class CDownloadListUI : public CTileLayoutUI
    {
    public:
         enum { SCROLL_TIMERID = 10 };
        CDownloadListUI(void);
        ~CDownloadListUI(void);
        virtual void DoEvent(TEventUI& event);
        void AddItem();
    
    /*
    @param strUrl 下载文件url地址
    @param strFileNmae 任务文件名
    */
    void AddItem(CDuiString strUrl,CDuiString strFilename); private: UINT m_uButtonState; POINT m_ptLastMouse; LONG m_dwDelayDeltaY; DWORD m_dwDelayNum; DWORD m_dwDelayLeft; }; inline double CalculateDelay(double state) { return pow(state, 2); }

    .cpp文件

    #include "StdAfx.h"
    #include "DownloadListUI.h"
    
    
    CDownloadListUI::CDownloadListUI(void)
        :m_uButtonState(0)
        , m_dwDelayDeltaY(0)
        , m_dwDelayNum(0)
        , m_dwDelayLeft(0)
    {
    }
    
    
    CDownloadListUI::~CDownloadListUI(void)
    {
    }
    
    void CDownloadListUI::DoEvent( TEventUI& event )
    {
        if( !IsMouseEnabled() && event.Type > UIEVENT__MOUSEBEGIN && event.Type < UIEVENT__MOUSEEND ) {
            if( m_pParent != NULL ) m_pParent->DoEvent(event);
            else CTileLayoutUI::DoEvent(event);
            return;
        }
    
        if( event.Type == UIEVENT_TIMER && event.wParam == SCROLL_TIMERID )
        {
            if( (m_uButtonState & UISTATE_CAPTURED) != 0 ) {
                POINT pt = m_pManager->GetMousePos();
                LONG cy = (pt.y - m_ptLastMouse.y);
                m_ptLastMouse = pt;
                SIZE sz = GetScrollPos();
                sz.cy -= cy;
                SetScrollPos(sz);
                return;
            }
            else if( m_dwDelayLeft > 0 ) {
                --m_dwDelayLeft;
                SIZE sz = GetScrollPos();
                LONG lDeltaY =  (LONG)(CalculateDelay((double)m_dwDelayLeft / m_dwDelayNum) * m_dwDelayDeltaY);
                if( (lDeltaY > 0 && sz.cy != 0)  || (lDeltaY < 0 && sz.cy != GetScrollRange().cy ) ) {
                    sz.cy -= lDeltaY;
                    SetScrollPos(sz);
                    return;
                }
            }
            m_dwDelayDeltaY = 0;
            m_dwDelayNum = 0;
            m_dwDelayLeft = 0;
            m_pManager->KillTimer(this, SCROLL_TIMERID);
            return;
        }
        if( event.Type == UIEVENT_MOUSEWHEEL )
        {
            LONG lDeltaY = 0;
            if( m_dwDelayNum > 0 ) lDeltaY =  (LONG)(CalculateDelay((double)m_dwDelayLeft / m_dwDelayNum) * m_dwDelayDeltaY);
            switch( LOWORD(event.wParam) ) {
            case SB_LINEUP:
                if( m_dwDelayDeltaY >= 0 ) m_dwDelayDeltaY = lDeltaY + 8;
                else m_dwDelayDeltaY = lDeltaY + 12;
                break;
            case SB_LINEDOWN:
                if( m_dwDelayDeltaY <= 0 ) m_dwDelayDeltaY = lDeltaY - 8;
                else m_dwDelayDeltaY = lDeltaY - 12;
                break;
            }
            if( m_dwDelayDeltaY > 100 ) m_dwDelayDeltaY = 100;
            else if( m_dwDelayDeltaY < -100 ) m_dwDelayDeltaY = -100;
            m_dwDelayNum = (DWORD)sqrt((double)abs(m_dwDelayDeltaY)) * 5;
            m_dwDelayLeft = m_dwDelayNum;
            m_pManager->SetTimer(this, SCROLL_TIMERID, 50U);
            return;
        }
        CTileLayoutUI::DoEvent(event);
    }
    
    void CDownloadListUI::AddItem()
    {
        CDialogBuilder builder;
        CContainerUI* pItem=static_cast<CContainerUI*>(builder.Create(L"Item_load.xml", 0));
        if ( pItem )
        {
            int i=GetCount();
            CDuiString strText;
            strText.Format(L"%02d", i+1);
            CControlUI* pControl=pItem->GetItemAt(0);
            if ( pControl )
                pControl->SetText(strText);
            pControl=pItem->GetItemAt(2);
            CProgressUI* pProgress=(CProgressUI*)pControl->GetInterface(L"Progress");
            if ( pProgress )
                pProgress->SetValue(i+1);
    
            pControl=pItem->GetItemAt(3);
            if ( pControl )
            {
                strText.Format(L"%02d:%02d:%02d", 1,1,1);
                pControl->SetText(strText);
            }
    
            pControl=pItem->GetItemAt(4);
            if ( pControl )
            {
                strText.Format(L"%dM/s", i+1);
                pControl->SetText(strText);
            }
    
            pControl=pItem->GetItemAt(5);
            if ( pControl )
            {
                strText.Format(L"%d%%", i+1);
                pControl->SetText(strText);
            }
            pControl=pItem->GetItemAt(6);
            if ( pControl )
                pControl->SetText(L"正在下载");
            pControl=pItem->GetItemAt(7);
            if ( pControl )
            {
                strText.Format(L"BtnLoad1%d", i);
                pControl->SetName(strText);
                pControl->SetTag(i);
            }
            pControl=pItem->GetItemAt(8);
            if ( pControl )
            {
                strText.Format(L"BtnLoad2%d", i);
                pControl->SetName(strText);
                pControl->SetTag(i);
            }
            pControl=pItem->GetItemAt(9);
            if ( pControl )
            {
                strText.Format(L"BtnLoad3%d", i);
                pControl->SetName(strText);
                pControl->SetTag(i);
            }
            Add(pItem);
        }
    }
    
    void CDownloadListUI::AddItem( CDuiString strUrl ,CDuiString strFilename)
    {
        CDialogBuilder builder;
        CContainerUI* pItem=static_cast<CContainerUI*>(builder.Create(L"Item_load.xml", 0));
    
        pItem->SetName(strUrl);
        
        if ( pItem )
        {
            int i=GetCount();
            CDuiString strText;
            strText.Format(L"%02d", i+1);
            CControlUI* pControl=pItem->GetItemAt(0);
            if ( pControl )
                pControl->SetText(strText);
    
            pControl=pItem->GetItemAt(1);
            if (pControl)
            {
                pControl->SetText(strFilename);
                pControl->SetToolTip(strFilename);
            }
    
            pControl=pItem->GetItemAt(2);
            CProgressUI* pProgress=(CProgressUI*)pControl->GetInterface(L"Progress");
            if ( pProgress )
                pProgress->SetValue(1);
    
    
            pControl=pItem->GetItemAt(5);
            if ( pControl )
            {
                strText.Format(L"%d%%", 1);
                pControl->SetText(strText);
            }
            pControl=pItem->GetItemAt(6);
            if ( pControl )
                pControl->SetText(L"正在下载");
            pControl=pItem->GetItemAt(7);
            if ( pControl )
            {
                strText.Format(L"BtnLoad1%d", i);
                pControl->SetName(strText);
                pControl->SetTag(i);
            }
            pControl=pItem->GetItemAt(8);
            if ( pControl )
            {
                strText.Format(L"BtnLoad2%d", i);
                pControl->SetName(strText);
                pControl->SetTag(i);
            }
            pControl=pItem->GetItemAt(9);
            if ( pControl )
            {
                strText.Format(L"BtnLoad3%d", i);
                pControl->SetName(strText);
                pControl->SetTag(i);
            }
            Add(pItem);
        }
    }

     把任务项写到Item_load.xml中

    <?xml version="1.0" encoding="UTF-8"?>
    <Window>
        <Container width="430" height="40" inset="2,2,2,2"><!--Container-->
            <Label float="true" pos="4,10,30,30" align="left" font="1" textcolor="#FFF9F9F9" />
            <Label text="软件名称" float="true" pos="34,10,110,30" endellipsis="true" textcolor="#FFF9F9F9" align="left" font="0" />
            <Progress float="true" pos="110,18,230,22" bkimage="pro1.jpg" foreimage="pro2.jpg" min="0" max="100" value="0"/> 
            <Label text="剩余时间" float="true" pos="110,24,160,54" textcolor="#FFF9F9F9" align="left" font="0" />
            <Label text="下载速度" float="true" pos="165,24,230,54" textcolor="#FFF9F9F9" align="left" font="0" />
            <Label text="下载进度" float="true" pos="235,14,270,54" textcolor="#FFF9F9F9" align="left" font="0" />
            <Label float="true" pos="280,14,350,54" textcolor="#FFF9F9F9" align="left" font="0" />
            <Button tooltip="取消" float="true" pos="370,16,383,28" normalimage="file='buttons.png' source='28,0,41,12'" hotimage="file='buttons.png' source='28,12,41,24'" pushedimage="file='buttons.png' source='28,24,41,36'" />
            <Button tooltip="暂停" float="true" pos="340,16,353,28" normalimage="file='buttons.png' source='14,0,27,12'" hotimage="file='buttons.png' source='14,12,27,24'" pushedimage="file='buttons.png' source='14,24,27,36'" />
            <Button tooltip="继续" visible="false" float="true" pos="340,16,353,28" normalimage="file='buttons.png' source='0,0,13,12'" hotimage="file='buttons.png' source='0,12,13,24'" pushedimage="file='buttons.png' source='0,24,13,36'" />
        </Container>
    </Window>

    二、把工作线程封装成一个类

    .h文件

    #ifndef _DOWNLOADFILETHREAD_H
    #define _DOWNLOADFILETHREAD_H
    #include   <windows.h>  
    #include   <process.h>  
    
    typedef struct
    {
        CString url;
        int current;
        CString speed;
        CString remaintime;
    }PARAMS, *PPARAMS;
    
    typedef struct
    {
        CString *sender;//url
        CURL *handle;
        double* downloadFileLength;
        int* resumeByte;
    }Progress_User_Data;
    
    class CDownloadFileThread
    {
    public:
        CDownloadFileThread(CString url,string filename);
        ~CDownloadFileThread();
    
        void InitTask();
    
        /** 
        开始运行线程  
        **/  
        void Start();  
    
        void Run(); 
    
        //暂停下载
        void PauseTask();
    
        //恢复下载
        void ResumeTask();
    
        //退出下载
        void ExitDownload();
    
        std::string UnicodeToANSI(const wstring& wstr);
    
        static size_t my_write_func(void *ptr, size_t size, size_t nmemb, FILE *stream);
    
        static int my_progress_func(void *progress_data,double t, /* dltotal */double d, /* dlnow */double ultotal,double ulnow);
    
    
        static unsigned int WINAPI ThreadFunction(LPVOID pParam); 
    
        static size_t nousecb(char *buffer, size_t x, size_t y, void *userdata);
    
        // Get the file size on the server
        double getDownloadFileLength(string url);
    
        // Get the local file size
        int  getLocalFileLength(string filepath);
    
        CString GetCurrentUrl();
    
    private:  
        HANDLE m_handle;
    
        HANDLE m_StartEvent;
    
        HANDLE m_EndEvent;
    
        unsigned int m_ThreadID;
    
        /*volatile*/ bool m_bIsCancel;//取消下载
    
        CURL* m_curl;//libcurl句柄
    
        FILE* m_outfile;//文件指针
    
        CString m_url;//下载地址
    
        string m_filename;//文件名
    
        double m_downloadFileLength;//服务器文件长度
    
        int m_resumeByte;//本地已下载的文件长度
    };
    #endif//_DOWNLOADFILETHREAD_H

    .cpp文件

    #include "stdafx.h"
    #include "DownloadFileThread.h"
    
    CDownloadFileThread::CDownloadFileThread(CString url,string filename)
    :m_url(url)
    ,m_curl(NULL)
    ,m_outfile(NULL)
    ,m_bIsCancel(false)
    ,m_filename(filename)
    ,m_downloadFileLength(-1)
    ,m_resumeByte(-1)
    {
        m_handle = (HANDLE)_beginthreadex(NULL,0,ThreadFunction,(void*)this,0,&m_ThreadID);
    
        m_StartEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
    
        m_EndEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
    
        curl_easy_init();
    }
    
    CDownloadFileThread::~CDownloadFileThread()
    {
    
        SetEvent(m_EndEvent);
    
    
        if (m_handle)
        {
            CloseHandle(m_handle);
        }
    
    
        if (m_StartEvent)
        {
            CloseHandle(m_StartEvent);
        }
    
        if (m_EndEvent)
        {
            CloseHandle(m_EndEvent);
        }
    }
    
    void CDownloadFileThread::Start()
    {
        SetEvent(m_StartEvent);
    }
    
    size_t CDownloadFileThread::my_write_func(void *ptr, size_t size, size_t nmemb, FILE *stream)
    {
        size_t nWrite =fwrite(ptr, size, nmemb, stream);
    
        if (nWrite == 0)
        {
            return CURL_WRITEFUNC_PAUSE;
        }
        else
        {
            return nWrite;
        }
    
        //return fwrite(ptr, size, nmemb, stream);
    } 
    
    int CDownloadFileThread::my_progress_func(void *progress_data,
                         double t, /* dltotal */
                         double d, /* dlnow */
                         double ultotal,
                         double ulnow)
    {
        //printf("%s %g / %g (%g %%)
    ", progress_data, d, t, d*100.0/t);
    
        Progress_User_Data *data = static_cast<Progress_User_Data *>(progress_data);
    
        char text[256] = {0};
        //sprintf_s(text,sizeof(text),"%d",d*100.0/t);
    
        char timeFormat[256]= {0};
    
        
        CURL *easy_handle = data->handle;
    
        // Defaults to bytes/second
        double speed;
        string unit = "B";
        int hours,minutes,seconds;
        curl_easy_getinfo(easy_handle, CURLINFO_SPEED_DOWNLOAD, &speed); // curl_get_info必须在curl_easy_perform之后调用
    
        if (speed != 0)
        {
            double leftTime = ((*data->downloadFileLength) - d - (*data->resumeByte)) / speed;
    
            hours = leftTime / 3600;
            minutes = (leftTime - hours * 3600) / 60;
            seconds = leftTime - hours * 3600 - minutes * 60;
        }
    
        sprintf_s(timeFormat, sizeof(timeFormat), "%02d:%02d:%02d", hours, minutes, seconds);
    
    
        if (speed > 1024 * 1024 * 1024)
        {
            unit = "G";
            speed /= 1024 * 1024 * 1024;
        }
        else if (speed > 1024 * 1024)
        {
            unit = "MB";
            speed /= 1024 * 1024;
        }
        else if (speed > 1024)
        {
            unit = "KB";
            speed /= 1024;
        }
    
        sprintf_s(text,sizeof(text),"%.2f%s/s",speed, unit.c_str());
    
    
        CString* url = (CString*)(data->sender);
        PARAMS params;
        params.url = url->GetBuffer();
        params.current = d*100.0/t;
        params.speed = text;
        params.remaintime = timeFormat;
    
        HWND hWnd = FindWindow( NULL , L"下载管理器" );
        if (hWnd)
        {
            ::SendMessage(hWnd,WM_UPDATEPROGRESS,0,(LPARAM)&params);
        }
        
        return 0;
    }
    
    std::string CDownloadFileThread::UnicodeToANSI( const wstring& wstr )
    {
        int unicodeLen = ::WideCharToMultiByte(CP_ACP,0,wstr.c_str(),-1,NULL,0, NULL ,NULL);
    
        if(unicodeLen == 0) return std::string("");
    
        char *pChar= new char[unicodeLen+1];
    
        memset(pChar , 0 , sizeof( char ) * (unicodeLen+1));
    
        ::WideCharToMultiByte(CP_ACP,0,wstr.c_str(),-1,pChar,unicodeLen, NULL ,NULL);
    
        pChar[unicodeLen]=0;
    
        string str = pChar;
    
        delete [] pChar;
        pChar=NULL;
    
        return str;
    }
    
    size_t CDownloadFileThread::nousecb(char *buffer, size_t x, size_t y, void *userdata)
    {
        (void)buffer;
        (void)userdata;
        return x * y;
    }
    
    void CDownloadFileThread::InitTask()
    {
        m_curl = curl_easy_init();
    }
    
    void CDownloadFileThread::PauseTask()
    {
        m_bIsCancel = true;
         curl_easy_pause(m_curl,CURLPAUSE_RECV);
    }
    
    void CDownloadFileThread::ResumeTask()
    {
        m_bIsCancel = false;
        if (m_curl)
        {
            curl_easy_pause(m_curl,CURLPAUSE_RECV_CONT);
        }
    }
    
    void CDownloadFileThread::ExitDownload()
    {
        curl_easy_pause(m_curl,CURLPAUSE_RECV);
        
    }
    
    CString CDownloadFileThread::GetCurrentUrl()
    {
        return m_url;
    }
    double CDownloadFileThread::getDownloadFileLength( string url ) { CURL *easy_handle = NULL; int ret = CURLE_OK; double size = -1; do { easy_handle = curl_easy_init(); if (!easy_handle) { break; } // Only get the header data ret = curl_easy_setopt(easy_handle, CURLOPT_URL, url.c_str()); ret |= curl_easy_setopt(easy_handle, CURLOPT_HEADER, 1L); ret |= curl_easy_setopt(easy_handle, CURLOPT_NOBODY, 1L); ret |= curl_easy_setopt(easy_handle, CURLOPT_WRITEFUNCTION, nousecb); // libcurl_a.lib will return error code 23 without this sentence on windows if (ret != CURLE_OK) { break; } ret = curl_easy_perform(easy_handle); if (ret != CURLE_OK) { char s[100] = {0}; sprintf_s(s, sizeof(s), "error:%d:%s", ret, curl_easy_strerror(static_cast<CURLcode>(ret))); break; } // size = -1 if no Content-Length return or Content-Length=0 ret = curl_easy_getinfo(easy_handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &size); if (ret != CURLE_OK) { break; } } while (0); curl_easy_cleanup(easy_handle); return size; } //C语言 下获取文件长度 int CDownloadFileThread::getLocalFileLength(string filepath) { FILE *fp=fopen(filepath.c_str(),"r"); if(!fp) return -1; fseek(fp,0L,SEEK_END); int size=ftell(fp); fclose(fp); return size; }
    void CDownloadFileThread::Run() { CURLcode res; char *progress_data = "* "; m_curl = curl_easy_init(); if(m_curl) { m_resumeByte = getLocalFileLength(m_filename); // Get the file size on the server m_downloadFileLength = getDownloadFileLength(UnicodeToANSI(m_url.GetBuffer())); if(m_resumeByte >= (int)m_downloadFileLength) return; m_outfile = fopen(m_filename.c_str(), "ab+"); Progress_User_Data data = { &m_url, m_curl,&m_downloadFileLength,&m_resumeByte}; curl_easy_setopt(m_curl, CURLOPT_URL, UnicodeToANSI(m_url.GetBuffer()).c_str());//在此要注意,此url必须是多字节 curl_easy_setopt(m_curl, CURLOPT_TIMEOUT, 0); curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, m_outfile); curl_easy_setopt(m_curl, CURLOPT_WRITEFUNCTION, my_write_func); curl_easy_setopt(m_curl, CURLOPT_NOPROGRESS, FALSE); curl_easy_setopt(m_curl, CURLOPT_PROGRESSFUNCTION, my_progress_func); curl_easy_setopt(m_curl, CURLOPT_PROGRESSDATA, &data);//给下载进度回调函数传参数,在此是指针 res = curl_easy_perform(m_curl); fclose(m_outfile); /* always cleanup */ curl_easy_cleanup(m_curl); } } unsigned int WINAPI CDownloadFileThread::ThreadFunction( LPVOID pParam ) { CDownloadFileThread* pthis = (CDownloadFileThread*)pParam; HANDLE hThrds[2]; hThrds[0] = pthis->m_StartEvent; hThrds[1] = pthis->m_EndEvent; while(true) { DWORD result = WaitForMultipleObjects(2,hThrds,FALSE, INFINITE); if (result == WAIT_OBJECT_0) { pthis->Run(); } else if (result == WAIT_OBJECT_0+1) { break; } } return 0; }

    因为下载任务比较耗时,所以在工作线程中进行下载,然后在UI线程显示进度,在此我用的是工作线程发消息给UI线程,然后进行进度显示。

    1.这里要给界面上任务Item设置一个控件ID,这样更新进度,可以知道具体更新哪个任务的进度

    2.界面添加任务时,创建一个下载对象,把url传给下载对象。

    3.工作线程拿到下载进度后,用SendMessage发给UI线程,发送时要把下载用的url传回去(其实也可以用索引作为作为ID)

    typedef struct
    {
        CString url;//控件ID
        int current;//进度
        CString speed;//下载速速
        CString remaintime;//下载剩余时间
    }PARAMS, *PPARAMS;
    typedef struct
    {
        CString *sender;//控件ID
        CURL *handle;libcurl指针
        double* downloadFileLength;//服务上文件长度
        int* resumeByte;//本地已下载文件长度
    }Progress_User_Data;

    通过url拿到文件名,这个不是所有的都可以拿到,因为有的url中没有名字

    void GetFileNameFormUrl( char* fileName, const char* url )
    {
        int urlLen = strlen(url);
        char mUrl[512] = {0};
        char fName[256] = {0};
        strcpy(mUrl, url);
        int cutIndex = 0;
        int i = urlLen - 1, j = 0;
        while(mUrl[--i] != '/');
        i++;
        while(mUrl[i] != '' && mUrl[i] != '?' &&mUrl[i] != '&')
        {
            fName[j++] = mUrl[i++];
        }
        fName[j] = '';
        strcpy(fileName, fName);
    
        return ;
    }

    更新进度的消息响应函数

    LRESULT CMainWnd::OnRefresh(UINT uMsg, WPARAM wParam, LPARAM lParam,BOOL& bHandled)
    {
        PARAMS* pParam = (PARAMS*)lParam;
        CDuiString strText;
    
        for (int i = 0; i < m_pDownloadList->GetCount();i++)
        {
            CControlUI* pControl = m_pDownloadList->GetItemAt(i);
    
            if ( _tcscmp(pParam->url.GetBuffer(), pControl->GetName())==0 )
            {
                CContainerUI* pContain = static_cast<CContainerUI*>(pControl);
    
    
                CControlUI* pControl=pContain->GetItemAt(2);
                CProgressUI* pProgress=(CProgressUI*)pControl->GetInterface(L"Progress");
                if ( pProgress )
                    pProgress->SetValue(pParam->current);
    
                pControl=pContain->GetItemAt(3);
                if ( pControl )
                {
                    pControl->SetText(pParam->remaintime);
                }
    
                pControl=pContain->GetItemAt(4);
                if ( pControl )
                {
                    
                    pControl->SetText(pParam->speed.GetBuffer());
                }
    
                pControl=pContain->GetItemAt(5);
                if ( pControl )
                {
                    strText.Format(L"%d%%", pParam->current);
                    pControl->SetText(strText);
                }
    
                break;
            }
        }
    
        return 0;
    }

    三、libcurl库支持下载过程中暂停,然后可以恢复下载,但在实际使用过程中遇到一个问题,暂停时间不长,可以正常恢复下载,如果暂停时间很长的话,再恢复时就没法继续下载了,这个问题还没解决。

    最后效果:

     

    下载源码Demo

  • 相关阅读:
    Git——pull拉取远程指定分支以及push到远程指定分支
    Git——拉取远程主分支到本地新建分支,并关联到对应的远程新分支
    Git——基础学习
    Flutter——侧边二级菜单栏
    Flutter——static, final, const 区别
    Git一些常用的指令
    Flutter——切换页面,如何保持当前页的状态
    Flutter—找不到图片&不显示本地图片
    Android Studio快捷方式
    某iOS APP反抓包分析
  • 原文地址:https://www.cnblogs.com/chechen/p/7339991.html
Copyright © 2011-2022 走看看