; --------------------------------------------------------- ; 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, "http://luchezl.free.fr/Autres/Boxe_Thai.rar") StringGadget(#Gadget_SaveTo, 70, 30, 320, 20, "F:\Documents and Settings\Lio\Bureau\test.rar") 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 Sleep_(200) Debug DeleteFile(SaveTo) Sleep_(200) Debug DeleteFile(SaveTo) 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://purebasic.myforums.net) 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 ; ---------------------------------------------------------