Delay(1) is enough to throttle back your app from claiming 100% cpu time.
Also i use threads for downloading file as this can allow your app to continue using the WaitWindowEvent() function while your download is active.
This is another possibility using interfaces to download a file with a progress callback. This really is an expert way of handling everthing and i really must start understanding interfaces!!!
Code: Select all
; ---------------------------------------------------------
; Backrgound downloading with UrlDownloadToFile_()
; Tutorial for creating a COM object with PB
;
; 10/09/2003 by Timo 'Fr34k' Harter
; http://freak.purearea.net
; ---------------------------------------------------------
;
; First, I want to say, that not everything here came from
; my mind, i had some input from a C++ program found here:
; http://www.codeproject.com/internet/urlfile.asp
;
; This tutorial is a complete PB executable, that can be executed
; as it is, with hopefully enough comments for you to understand.
;
; Intro:
; Ok, much people know the UrlDownloadToFile_() Api, which is
; a very comfortable way to download a file, because you don't
; have to worry about the protocol you use, and reading headers
; and stuff with raw network commands.
; Now the problem with that command was, no easy way display the
; status of the download operation. This is possible by creating
; an own IBindStatusCallback Interface to handle this. Now actually
; you don't need any of PB's new Interface stuff to do this, as you
; can see in this code. Only till now i didn't have the knowledge how
; to do this.
; I will show here, how to create an object with a IBindStatusCallback
; Interface, and how to do a nice background downloaading with that.
;
; But that is unfortunately not all. UrlDownloadToFile_() stops the
; program flow, till the download is done, and we need a way around
; that. To do this, we put the function in a seperate thread. The
; problem then is, that the methods of our IBindStatusCallback
; Interface are then also called in this thread's conext, and so we
; can't update our user interface from there, as it is in a different
; thread. So, in order to communicate between the threads, we use
; SendMessage_() and send 2 userdefined messages.
;
; To get more info on UrlDownLoadToFile_(), go here:
; http://msdn.microsoft.com/library/default.asp?url=/library/en-us/wceurlmn/html/cerefurldownloadtofile.asp
;
; Read more about the IBindStatusCallback Interface here:
; http://msdn.microsoft.com/library/default.asp?url=/library/en-us/guide_ppc/htm/urlmon1_rgqn.asp
;
; So much for the general way this program functions, let's get started...
;
; ---------------------------------------------------------
; These Global variables are used by the thread to start the download, and
; to post the messages, so they may only be manipulated BEFORE a download is
; started, changing them while a download is running may have unexpected
; results (most likely a crash)
Global Url.s, SaveTo.s, MainWindow.l
; Url is the Source address. May be something like "www.purebasic.com" or a direct file.
; SaveTo is the target filename on disk
; MainWindow is a WindowID() of an open Window, where the messages will be sent to
; This value is Global, but not used from the thread. We use it to indicate, that
; the download should be aborted (if it is #TRUE)
Global Abort.l
; This structure is used to communicate between the thread and the WindowCallback
Structure ProgressData
 Progress.l  ; bytes downloaded
 ProgressMax.l ; bytes total (this value might change during the download)
 StatusCode.l ; A code indicating what is happening
EndStructure
Structure IIDÂ ; Interface Identifier structure. a IID is a 16byte value, that uniquely
 Data1.l    ; identifys each interface.
 Data2.w
 Data3.w
 Data4.b[8]
EndStructure
; Now these are the 2 messages we send. One to indicate a progress status
; and one to inbdicate the download end. Values above #WM_USER are free for use
; inside programs.
#WM_DOWNLOADPROGRESS = #WM_USER + 1
#WM_DOWNLOADENDÂ Â Â = #WM_USER + 2
; these are the values that StatusCode.l of the ProgressData Structure might get.
; Note: as IBindStatusCallback can also be used for other things than downloads,
; some of these values may never occur with UrlDownloadToFile_()
; Go here for more info on those values:
; http://msdn.microsoft.com/library/default.asp?url=/library/en-us/wceurlmn/html/cerefBINDSTATUS.asp
Enumeration 1
 #BINDSTATUS_FINDINGRESOURCE
 #BINDSTATUS_CONNECTING
 #BINDSTATUS_REDIRECTING
 #BINDSTATUS_BEGINDOWNLOADDATA
 #BINDSTATUS_DOWNLOADINGDATA
 #BINDSTATUS_ENDDOWNLOADDATA
 #BINDSTATUS_BEGINDOWNLOADCOMPONENTS
 #BINDSTATUS_INSTALLINGCOMPONENTS
 #BINDSTATUS_ENDDOWNLOADCOMPONENTS
 #BINDSTATUS_USINGCACHEDCOPY
 #BINDSTATUS_SENDINGREQUEST
 #BINDSTATUS_CLASSIDAVAILABLE
 #BINDSTATUS_MIMETYPEAVAILABLE
 #BINDSTATUS_CACHEFILENAMEAVAILABLE
 #BINDSTATUS_BEGINSYNCOPERATION
 #BINDSTATUS_ENDSYNCOPERATION
 #BINDSTATUS_BEGINUPLOADDATA
 #BINDSTATUS_UPLOADINGDATA
 #BINDSTATUS_ENDUPLOADINGDATA
 #BINDSTATUS_PROTOCOLCLASSID
 #BINDSTATUS_ENCODING
 #BINDSTATUS_VERFIEDMIMETYPEAVAILABLE
 #BINDSTATUS_CLASSINSTALLLOCATION
 #BINDSTATUS_DECODING
 #BINDSTATUS_LOADINGMIMEHANDLER
 #BINDSTATUS_CONTENTDISPOSITIONATTACH
 #BINDSTATUS_FILTERREPORTMIMETYPE
 #BINDSTATUS_CLSIDCANINSTANTIATE
 #BINDSTATUS_IUNKNOWNAVAILABLE
 #BINDSTATUS_DIRECTBIND
 #BINDSTATUS_RAWMIMETYPE
 #BINDSTATUS_PROXYDETECTING
 #BINDSTATUS_ACCEPTRANGES
EndEnumeration
; ---------------------------------------------------------
; StatusObject
; ---------------------------------------------------------
; Ok, now we implement our IBindStatusCallback Interface. The
; object we create it in i call 'StatusObject'.
;
; Let's first discuss how such an object looks like. Basically, it
; is a structure containing pointers to other structures (which represent
; the interfaces), which themselves contain pointers To functions.
; (which are the methods of this interface)
;
; It is not as complicated as it sounds, let's take it step by step:
; First we need to know how the Interface we want looks like. There will
; be a comfortable InterfaceViewer soon, but for now, you have to peek in
; in the *.pb files at http://cvs.purebasic.com (/Residents/Windows/Interface)
; The important thing is to get the order of the methods right (methods are
; simply the functions of a interface)
;
; IBindStatusCallback looks like this:
;
; Interface IBindStatusCallback
;Â Â QueryInterface(a.l, b.l)
;Â Â AddRef()
;Â Â Release()
;Â Â OnStartBinding(a.l, b.l)
;Â Â GetPriority(a.l)
;Â Â OnLowResource(a.l)
;Â Â OnProgress(a.l, b.l, c.l, d.l)
;Â Â OnStopBinding(a.l, b.l)
;Â Â GetBindInfo(a.l, b.l)
;Â Â OnDataAvailable(a.l, b.l, c.l, d.l)
;Â Â OnObjectAvailable(a.l, b.l)
; EndInterface
; Now first, we need a Structure, that can hold pointers to all our
; functions for this interface, this looks almost the same then:
Structure IBindStatusCallback_Functions
 QueryInterface.l
 AddRef.l
 Release.l
 OnStartBinding.l
 GetPriority.l
 OnLowResource.l
 OnProgress.l
 OnStopBinding.l
 GetBindInfo.l
 OnDataAvailable.l
 OnObjectAvailable.l
EndStructure
; let's make a structured variable out of it. We will fill the pointers in,
; after we created the functions.
IBindStatusCallback_Functions.IBindStatusCallback_Functions
; This is called the Virtual Table, it is where the caller to our Interface
; will find the addresses of the method (function) he want's to call.
; Now an interface is always part of an object. An object can contain multiple
; interfaces, or just one. The object is again a structure, that then contains
; the pointer to the virtual table of our interface:
Structure StatusObject
 *IBindStatusCallback.IBindStatusCallback_Functions
EndStructure
; We have only one interface in there. Well actually 2, because IBindStatusCallback
; has the IUnknown interface inside. The structure can also hold extra data fields,
; that our functions can access to store data for this object, but we don't need
; that now.
; Let's make also a structured variable for our object. We can pass the pointer to
; that variable to everybody who want's to call our interface then. It has to be
; Global, so it is also known inside the thread.
Global StatusObject.StatusObject
; set the pointer to the virtual table of our interface:
StatusObject\IBindStatusCallback = IBindStatusCallback_Functions
; ---------------------------------------------------------
; Now we can create the methods for our interface.
; Note: It is quite simple: We create one Procedure for each method, with the
; arguments the method needs (look at the description of the Interface), with the
; only addition that each Procedure has a *THIS.MyObject pointer as first value.
; There it always get's the pointer to the object on which it is called. In the
; documentation by MS for eaxample, this is never mentioned, because in other
; languages, this parameter is hidden, but it is always there.
; For what the method should do and return, see the documentation.
; QueryInterface is a method that comes from the IUnknown interface, and is called,
; in order to get different interfaces on one object. We need to check the IID that
; is provided, and return the right pointer. As we only have one Interface with
; an IUnknown inside, this is quite simple:
Procedure.l StatusObject_QueryInterface(*THIS.StatusObject, *iid.IID, *Object.LONG)
 ; compare the IID to the IID's in our DataSection
 If CompareMemory(*iid, ?IID_IUnknown, SizeOf(IID)) Or CompareMemory(*iid, ?IID_IBindStatusCallback, SizeOf(IID))
Â
  ; return the object itself. See this is why this *THIS pointer is usefull
  *Object\l = *THIS Â
  ProcedureReturn #S_OK
 Else Â
 Â
  ; Ok, the caller requests an interface we don't have, so let's tell him:
  *Object\l = 0
  ProcedureReturn #E_NOINTERFACE
 EndIf
EndProcedure
; In AddRef we just have to increase a counter, of how much references exist to
; our object, and return that number:
Procedure.l StatusObject_AddRef(*THIS.StatusObject)
 Shared StatusObject_Count.l
 StatusObject_Count + 1
 ProcedureReturn StatusObject_Count
EndProcedure
; Release is the same the other way around:
Procedure.l StatusObject_Release(*THIS.StatusObject)
 Shared StatusObject_Count.l
 StatusObject_Count - 1
 ProcedureReturn StatusObject_Count
EndProcedure
; ---------------------------------------------------------
; Ok, now for the IBindStatusCallback specific methods:
; We basically only need the OnProgress method, so we just return
; #S_OK everywhere we don't need to take any action, and #E_NOTIMPL, where
; we would need to do something (to tell that we didn't implement the method)
Procedure.l StatusObject_OnStartBinding(*THIS.StatusObject, Reserved.l, *IB.IBinding)
 ProcedureReturn #S_OK
EndProcedure
Procedure.l StatusObject_GetPriority(*THIS.StatusObject, *Priority.LONG)
 ProcedureReturn #E_NOTIMPL
EndProcedure
Procedure.l StatusObject_OnLowResource(*THIS.StatusObject)
 ProcedureReturn #E_NOTIMPL
EndProcedure
; Now we come to the interresting part: OnProgress
; Remember: this is called from inside the second thread, so we can't use
; any Strings in there for example. We basically just pass on every information
; to the main thread via SendMessage, and do nothing else here:
Procedure.l StatusObject_OnProgress(*THIS.StatusObject, Progress.l, ProgressMax.l, StatusCode.l, szStatusText.l)
 ; Make a ProgressData structure, fill it with the information we have:
 ProgressData.ProgressData
 ProgressData\Progress = Progress
 ProgressData\ProgressMax = ProgressMax
 ProgressData\StatusCode = StatusCode
Â
 ; szStatusText contains additional information, unfortunately, in UNICODE format.
 ; So we have to convert it. For more information on that, search the forum, there
 ; are several examples for UNICODE strings.
Â
 ; get length of string
 Length = WideCharToMultiByte_(#CP_ACP, 0, szStatusText, -1, 0, 0, 0, 0)
Â
 ; now we allocate some memory for that string, we can't use AllocateMemory(), because
 ; it requeres a fixed number, we don't want to use.
 *String = HeapAlloc_(GetProcessHeap_(), 0, Length)
Â
 ; convert string Â
 WideCharToMultiByte_(#CP_ACP, 0, szStatusText, -1, *String, Length, 0, 0)
Â
 ; we use SendMessage to send the information, the address of the ProgressData
 ; structure as wParam, and the address of the string as lParam.
 ; SendMessage waits until the WindowCallback of the main thread has processed
 ; the message, so the threads are syncronized like that, and we can destroy our
 ; string afterwards.
 Result = SendMessage_(MainWindow, #WM_DOWNLOADPROGRESS, @ProgressData, *String)
Â
 ; free the string
 HeapFree_(GetProcessHeap_(), 0, *String)
Â
 ; From the Windowcallback, we return the value of the Global 'Abort' variable. If it
 ; is #TRUE, we return #E_ABORT here, to stop the download:
 If Result = #TRUE
  ProcedureReturn #E_ABORT
 Else
  ProcedureReturn #S_OK
 EndIf
EndProcedure
; another couple of unused methods, but they need to be there:
Procedure.l StatusObject_OnStopBinding(*THIS.StatusObject, Result.l, szError.l)
 ProcedureReturn #S_OK
EndProcedure
Procedure.l StatusObject_GetBindInfo(*THIS.StatusObject, BINDF.l, *bindinfo)
 ProcedureReturn #S_OK
EndProcedure
Procedure.l StatusObject_OnDataAvailable(*THIS.StatusObject, BSCF.l, Size.l, *formatec, *stgmed)
 ProcedureReturn #S_OK
EndProcedure
Procedure.l StatusObject_OnObjectAvailable(*THIS.StatusObject, *iid.IID, *UNK.IUnknown)
 ProcedureReturn #S_OK
EndProcedure
; ---------------------------------------------------------
; Ok, now that all methods are there, we fill the virtual table with the
; addresses:
IBindStatusCallback_Functions\QueryInterface  = @StatusObject_QueryInterface()
IBindStatusCallback_Functions\AddRef      = @StatusObject_AddRef()
IBindStatusCallback_Functions\Release      = @StatusObject_Release()
IBindStatusCallback_Functions\OnStartBinding  = @StatusObject_OnStartBinding()
IBindStatusCallback_Functions\GetPriority    = @StatusObject_GetPriority()
IBindStatusCallback_Functions\OnLowResource   = @StatusObject_OnLowResource()
IBindStatusCallback_Functions\OnProgress    = @StatusObject_OnProgress()
IBindStatusCallback_Functions\OnStopBinding   = @StatusObject_OnStopBinding()
IBindStatusCallback_Functions\GetBindInfo    = @StatusObject_GetBindInfo()
IBindStatusCallback_Functions\OnDataAvailable  = @StatusObject_OnDataAvailable()
IBindStatusCallback_Functions\OnObjectAvailable = @StatusObject_OnObjectAvailable()
; Here's the DataSection with the IID's for IUnknown and IBindStatusCallback
; I put them here, because they belong to the Interface stuff, not to the GUI part.
DataSection
 IID_IUnknown: ; {00000000-0000-0000-C000-000000000046}
  Data.l $00000000
  Data.w $0000, $0000
  Data.b $C0, $00, $00, $00, $00, $00, $00, $46
 Â
 IID_IBindStatusCallback: ; {79eac9c1-baf9-11ce-8c82-00aa004ba90b}
  Data.l $79eac9c1
  Data.w $baf9, $11ce
  Data.b $8c, $82, $00, $aa, $00, $4b, $a9, $0b Â
EndDataSection
; That was actually all that was there to do to implement a IBindStatusCallback
; Interface in our program. We now have a 'StatusObject' object structure containing
; our Interface. That's all we need.
; GUI part comes next. Let's first create a nice GUI with the Visual Designer:
; ---------------------------------------------------------
; PureBasic Visual Designer v3.80 build 1249
; Window Constants
;
Enumeration
 #DownloadWindow
EndEnumeration
; Gadget Constants
;
Enumeration
 #Gadget_1
 #Gadget_2
 #Gadget_Url
 #Gadget_SaveTo
 #Gadget_ChooseFile
 #Gadget_Status
 #Gadget_Progress
 #Gadget_Start
 #Gadget_Stop
 #Gadget_Close
 #Gadget_StatusText
EndEnumeration
Procedure Open_DownloadWindow()
 If OpenWindow(#DownloadWindow, 414, 385, 447, 230, #PB_Window_SystemMenu | #PB_Window_TitleBar | #PB_Window_ScreenCentered , "File download:")
  If CreateGadgetList(WindowID())
   TextGadget(#Gadget_1, 5, 10, 60, 20, "Url:", #PB_Text_Right)
   TextGadget(#Gadget_2, 5, 35, 60, 20, "SaveTo:", #PB_Text_Right)
   StringGadget(#Gadget_Url, 70, 5, 320, 20, "")
   StringGadget(#Gadget_SaveTo, 70, 30, 320, 20, "")
   ButtonGadget(#Gadget_ChooseFile, 395, 30, 50, 20, "...")
   ListViewGadget(#Gadget_Status, 5, 55, 385, 120)
   ProgressBarGadget(#Gadget_Progress, 5, 180, 385, 20, 0, 100)
   ButtonGadget(#Gadget_Start, 395, 80, 50, 20, "Start")
   ButtonGadget(#Gadget_Stop, 395, 105, 50, 20, "Abort")
   ButtonGadget(#Gadget_Close, 395, 205, 50, 20, "Close")
   TextGadget(#Gadget_StatusText, 5, 205, 385, 20, "", #PB_Text_Center | #PB_Text_Border)
  Â
  EndIf
 EndIf
EndProcedure
; ---------------------------------------------------------
; Ok, next we need a Procedure for our thread. It does nothing than
; call the UrlDownloadToFile_() function with our Global settings and, of course
; our Interface, and then SendMessage the result back to the main thread.
; A thread procedure MUST have one argument, but as we don't need
; it, we call it Dummy.
Procedure BackgroundDownload(Dummy.l)
 Result.l = URLDownloadToFile_(0, @Url, @SaveTo, 0, @StatusObject)
 SendMessage_(MainWindow, #WM_DOWNLOADEND, 0, Result)
EndProcedure
; Next is the WindowCallback procedure. Here we handle, what comes back from
; our OnProgress method, and from the thread procedure:
Procedure WindowCallback(Window.l, Message.l, wParam.l, lParam.l)
 Result.l = #PB_ProcessPureBasicEvents
Â
 ; download is in progress...
 If Message = #WM_DOWNLOADPROGRESS
 Â
  ; in wParam, we habe a pointer to the infor structure:
  *Progress.ProgressData = wParam
 Â
  ; let's update the ProgressBar:
  ; Progress may be always equal to ProgressMax, for example if the real size
  ; is unknown.
  If *Progress\Progress = *Progress\ProgressMax Or *Progress\ProgressMax = 0
   SetGadgetState(#Gadget_Progress, 0)
  Else
   SetGadgetState(#Gadget_Progress, (*Progress\Progress*100)/*Progress\ProgressMax)
  EndIf
 Â
  ; a pointer to the extra text is in lParam:
  StatusText.s = PeekS(lParam)
 Â
  ; now we check those StatusCodes, that are used for downloads, and set up a nice
  ; message:
  Select *Progress\StatusCode
   Case #BINDSTATUS_FINDINGRESOURCE:  Text.s = "Finding "+StatusText
   Case #BINDSTATUS_CONNECTING:    Text.s = "Connecting to "+StatusText   Â
   Case #BINDSTATUS_REDIRECTING:    Text.s = "Resolved to "+StatusText
   Case #BINDSTATUS_BEGINDOWNLOADDATA: Text.s = "Downloading "+StatusText
   Case #BINDSTATUS_ENDDOWNLOADDATA:  Text.s = "Finished downloading "+StatusText
   Case #BINDSTATUS_USINGCACHEDCOPY:  Text.s = "Receiving file from cache."
   Case #BINDSTATUS_MIMETYPEAVAILABLE: Text.s = "MIME Type is "+StatusText
   Case #BINDSTATUS_PROXYDETECTING:  Text.s = "A Proxy Server was detected"
   Default: Text.s = ""
  EndSelect
  If Text <> ""
   AddGadgetItem(#Gadget_Status, -1, Text)
  EndIf
 Â
  ; scroll down to the end:
  SetGadgetState(#Gadget_Status, CountGadgetItems(#GAdget_Status)-1)
 Â
  ; Set the sizes also in our TextGadget
  SetGadgetText(#Gadget_StatusText, Str(*Progress\Progress) + " of " + Str(*Progress\ProgressMax) + " Bytes complete")
 Â
  ProcedureReturn Abort
 Â
 ; download finished:
 ; Note: there is also a StatusCode for finished, but it is not sent on errors, so
 ; we also need this one:
 ElseIf Message = #WM_DOWNLOADEND
Â
  ; lParam contains the result of the UrlDownLoadToFile_() Api:
  If lParam = #S_OK   Â
   ;jippeeeee :)
   AddGadgetItem(#Gadget_Status, -1, "Download complete.")
   SetGadgetState(#Gadget_Progress, 100)
  Else
   ; damn :(
   AddGadgetItem(#Gadget_Status, -1, "Download failed!!")
   SetGadgetState(#Gadget_Progress, 0)
  EndIf
  SetGadgetState(#Gadget_Status, CountGadgetItems(#GAdget_Status)-1) Â
 Â
  ; switch Start/Stop button:
  DisableGadget(#Gadget_Start, #FALSE)
  DisableGadget(#Gadget_Stop, #TRUE)       Â
 Â
 EndIf
Â
 ProcedureReturn Result
EndProcedure
; ---------------------------------------------------------
; Now that's finally where our program starts:
; open the window and set the WindowCallback:
Open_DownloadWindow()
SetWindowCallback(@WindowCallback())
; who needs an 'abort' button now?
DisableGadget(#Gadget_Stop, #TRUE)
; A nice little extra for the StringGadgets: AutoComplete feature
; only present on IE5+, so we load the function manually:
#SHACF_URLALL = 2|4
#SHACF_FILESYSTEM = 1
CoInitialize_(0)
If OpenLibrary(0, "shlwapi.dll")Â
 CallFunction(0, "SHAutoComplete", GadgetID(#Gadget_Url), #SHACF_URLALL)
 CallFunction(0, "SHAutoComplete", GadgetID(#Gadget_SaveTo), #SHACF_FILESYSTEM)Â
 CloseLibrary(0)
EndIf
; finally: the main loop:
Repeat
 Select WaitWindowEvent()
  Case #PB_EventCloseWindow: End
  Case #PB_EventGadget
   Select EventGadgetID()
    Case #Gadget_Close: End
   Â
    Case #Gadget_ChooseFile
     File.s = SaveFileRequester("Save File to...", GetGadgetText(#Gadget_SaveTo), "All Files|*.*", 0)
     If File <> "": SetGadgetText(#Gadget_SaveTo, File): EndIf
    Â
    ; download starts:
    Case #Gadget_Start
     ; set Abort to false, so our download doesn't get stopped imediately
     Abort = #FALSE
    Â
     ; switch start/stop
     DisableGadget(#Gadget_Start, #TRUE)
     DisableGadget(#Gadget_Stop, #FALSE)
    Â
     ; cleat gadgets:
     SetGadgetState(#Gadget_Progress, 0)
     ClearGadgetItemList(#Gadget_Status)
    Â
     ; set our global values:
     Url = GetGadgetText(#Gadget_Url)
     SaveTo = GetGadgetText(#Gadget_SaveTo)
    Â
     ; this one is important for our messages to work:
     MainWindow = WindowID(#DownloadWindow)
    Â
     ; finally, start the download by creating the thread:
     CreateThread(@BackgroundDownload(), 0)
    Â
    Case #Gadget_Stop
   Â
     ; to stop, we set Abort to #TRUE, and on the next time, the
     ; OnProgress method get's called, the download is aborted.
     Abort = #TRUE
   Â
   EndSelect
 EndSelect
ForEver
; ---------------------------------------------------------
;
; WOW, now my fingers really hurt!
;
; I hope, you were able to understand all the stuff i was talking here,
; and that it helps you getting into COM (and doing nice downloads).
; If you have further questions, feel free to ask me or anybody else on
; the PureBasic forums (http://jconserv.net/purebasic) or send me an
; email (freak@purearea.net)
;
; btw: forgive me for all the typos, but there is unfortunately no spell
; checking feature in the PB Editor :D ... and english is not my native language.
; (well, i doubt that my german is much better though :) )
;
;
; Timo
; ---------------------------------------------------------