Page 1 of 1

How to update UI gadgets inside a procedure

Posted: Mon Nov 10, 2025 10:26 am
by susan
Inside a procedure, I want to update the UI to tell the user to wait.

Code: Select all

Procedure on_takeslongtime(EventType)
  
  SetGadgetText(pGadget, "Please wait while this takes a long time...")
  ; WindowEvent() thought this might allow the gadgets to update
  ; Delay(10) or maybe this?
  ; Sleep(1) I found this with a web search, and it looked promising but it is no longer part of Purebasic
  For index = 0 To 100000
    ; doing something that takes many seconds
  Next
  
EndProcedure
But the gadget shows the text when the procedure has finished. I looked in the Help and searched online, and tried a few ideas (shown in the code comments above), but I can't figure out how to make the SetGadgetText be shown before the loop begins, and before the procedure ends.

Am I missing something, or is it not possible, and I need to rethink how the procedures are organised?

Re: How to update UI gadgets inside a procedure

Posted: Mon Nov 10, 2025 12:11 pm
by Axolotl
That question leads to event handling. I can answer this for windows only. Still too far away from Linux.

Maybe this can help you.
Handling Events - two different ways


Easy answer: You can add this in the loop.

Code: Select all

  While WindowEvent() : Wend 
Important Note:
You remove events from the System-Message-Loop. So any action on your app are lost.

I prefer to use something like this, because I want to cancel long running loops (mostly).

Code: Select all

Procedure PerformEvents(Gadget=-1)  ; BOOL .. e.g. Gadget == Cancel - Button 
  Repeat 
    Select WindowEvent() 
      Case #PB_Event_None 
        ProcedureReturn #False 

      Case #PB_Event_Gadget 
        If IsGadget(Gadget) And EventGadget() = Gadget   ; Gadget == Cancel - Button 
          ProcedureReturn #True 
        EndIf 
    EndSelect 
    Delay(1)  ; 
  ForEver 

  If GetAsyncKeyState_(#VK_ESCAPE) & $8000  ; check the ESC key 
    ProcedureReturn #True 
  EndIf 
  ProcedureReturn #False 
EndProcedure 

CompilerIf 0 ; Call it like: 
  For index = 0 To 999999  
    If PerformEvents(#GDT_btnCancel) ; #GDT_btnCancel is a already created ButtonGadget() 
      bCanceledByUser = #True   ; set flag to handle the user wish later .... 
      Break                     ; leave the loop 
    EndIf 
    ; .....
    ; do the stuff 
    , ......
  Next index 
CompilerEndIf 


Re: How to update UI gadgets inside a procedure

Posted: Tue Nov 11, 2025 1:44 am
by susan
Axolotl wrote: Mon Nov 10, 2025 12:11 pm Easy answer: You can add this in the loop.

Code: Select all

  While WindowEvent() : Wend 
Important Note:
You remove events from the System-Message-Loop. So any action on your app are lost.
Thank you for the tips and link. Changing the lines to these works exactly as I hoped.

Code: Select all

Procedure on_takeslongtime(EventType)
  
  SetGadgetText(pGadget, "Please wait while this takes a long time...")
  While WindowEvent() : Wend
  For index = 0 To 100000
    ; doing something that takes many seconds
  Next
  
EndProcedure
I will keep the more complex lines that allows for cancelling the action, as an idea for the future. With the binding technique, that also might be for later after I have mastered the event loops because I want to start at the beginning of PureBasic and do everything the most basic way, just because.

I have since found that the While WindowEvent() : Wend technique has been mentioned many times before on the forum. Part of my problem, right now, is knowing the best words to search for in both the Help and online to find solutions. It feels like I still have a lot to absorb (and understand!) before I can efficiently find my own answers to problems.

Re: How to update UI gadgets inside a procedure

Posted: Tue Nov 11, 2025 8:58 am
by infratec
All UI stuff should be handled in the (one and only) Event loop.
If you want to do stuff from a procedure you should use PostEvent().
If your procedure takes longer ... this procedure should be an own thread to avoid blocking of the program,
which is a bad user experience.

Re: How to update UI gadgets inside a procedure

Posted: Tue Nov 11, 2025 9:29 am
by susan
infratec wrote: Tue Nov 11, 2025 8:58 am All UI stuff should be handled in the (one and only) Event loop.
If you want to do stuff from a procedure you should use PostEvent().
If your procedure takes longer ... this procedure should be an own thread to avoid blocking of the program,
which is a bad user experience.
I have no reason to think that these are not good advice, but...

I am struggling to learn everything at once. For example, I did look at the Threads related topics here and in the help and could see the potential. But just like I can see that lists are probably better than using a big array for my data, for now, I am being expedient to get used to forms and organising the lines, and even only just figuring out how to name things in a way that looks easy to read and makes sense to me.

My goal is to get something non-trivial working. It is a utility for personal use and as a learning exercise, and if the window stops responding for a few seconds that's ugly but okay. An argument can be made for doing things the "best" (right?) way the first time, but that may have to wait for my second or third attempt.

I have not heard of PostEvent() before, but that sounds like it will be useful, I appreciate the tips!

Re: How to update UI gadgets inside a procedure

Posted: Tue Nov 11, 2025 12:07 pm
by infratec

Code: Select all

EnableExplicit


Enumeration CustomEvents #PB_Event_FirstCustomValue
  #CustomEvent_ShowGadgetInteger
  #CustomEvent_ShowGadgetText
EndEnumeration



Procedure on_takeslongtime(*Dummy)
  
  Protected.i index
  
  
  PostEvent(#CustomEvent_ShowGadgetText, 0, 0, #PB_EventType_Change, @"Please wait while this takes a long time...")
  
  For index = 0 To 100000
    PostEvent(#CustomEvent_ShowGadgetInteger, 0, 1, #PB_EventType_Change, index)
    Delay(1000)
  Next
  
EndProcedure


Define.i Event, Quit

If OpenWindow(0, 0, 0, 300, 60, "PureBasic Window", #PB_Window_SystemMenu | #PB_Window_MinimizeGadget | #PB_Window_MaximizeGadget |#PB_Window_ScreenCentered)
  
  TextGadget(0, 10, 10, 200, 20, "")
  TextGadget(1, 220, 10, 50, 20, "", #PB_Text_Border)
  
  CreateThread(@on_takeslongtime(), #Null)
  
  Repeat
    Event = WaitWindowEvent()
    
    Select Event
      Case #CustomEvent_ShowGadgetInteger
        Select EventType()
          Case #PB_EventType_Change
            SetGadgetText(EventGadget(), Str(EventData()))
        EndSelect
        
      Case #CustomEvent_ShowGadgetText
        Select EventType()
          Case #PB_EventType_Change
            SetGadgetText(EventGadget(), PeekS(EventData()))
        EndSelect
      
      Case #PB_Event_CloseWindow  ; If the user has pressed on the close button
        Quit = #True
    EndSelect
    
  Until Quit
  
EndIf


Re: How to update UI gadgets inside a procedure

Posted: Tue Nov 11, 2025 8:59 pm
by susan
infratec wrote: Tue Nov 11, 2025 12:07 pm

Code: Select all


;more code

Procedure on_takeslongtime(*Dummy)
  
  Protected.i index
  
  
  PostEvent(#CustomEvent_ShowGadgetText, 0, 0, #PB_EventType_Change, @"Please wait while this takes a long time...")
  
  For index = 0 To 100000
    PostEvent(#CustomEvent_ShowGadgetInteger, 0, 1, #PB_EventType_Change, index)
    Delay(1000)
  Next
  
EndProcedure

;more code
Thank you for the example, that looks easy! (I didn't know that Delay allowed events to continue.)

Re: How to update UI gadgets inside a procedure

Posted: Tue Nov 11, 2025 10:18 pm
by spikey
susan wrote: Tue Nov 11, 2025 8:59 pm Thank you for the example, that looks easy! (I didn't know that Delay allowed events to continue.)
It doesn't. What infratec is doing in this example is creating a thread which runs alongside the main program. It uses PostEvent to tell the main thread that a UI update is appropriate. The main thread processes the events as usual but is never blocked by the worker thread (or isn't if the program is bug free!).

Threading is the only way to make a fully concurrent program but comes with some additional implications. There are some problems that you will face in a multi-threaded application that just don't appear in single threaded one, and debugging multi-threaded programs is harder than single-threaded ones.

If you haven't studied threaded programming before I'd strongly recommend some background reading first, it will save you some headaches later. For example, https://algocademy.com/blog/introductio ... ogramming/ is a reasonable introduction. It's not talking about PureBasic but the problems are the same and the solutions too, with the exception of 'Monitor's which PB doesn't provide intrinsically (but can be implemented).

Re: How to update UI gadgets inside a procedure

Posted: Wed Nov 12, 2025 12:04 am
by mk-soft
The example is kept very simple and some important things about threads are missing. But more on that later (it's late).

In the main thread where the GUI runs (WaitWindowEvent), it must not be interrupted by a Delay() or long processes,
as this is where all events from the OS and user input are passed to the user programme.
And there are a lot of events. Mouse, keyboard, changes to windows and controls.
These must always have passed through the main thread. Otherwise, a message such as ‘The programme is not responding’ will appear.

Re: How to update UI gadgets inside a procedure

Posted: Wed Nov 12, 2025 3:25 am
by susan
Okay, I confess I didn't try to run @infratec example, and if I had, what is does might have been more obvious, (and hopefully I would have examined the code more carefully).

For now I am happy with this trick.

Code: Select all

While WindowEvent() : Wend 
No, I have not used threads before, and I come from using some JavaScript in the browser and a little Python on a server, with only basic Python skills, where I have only scratched the surface.

I picked PureBasic because I thought it would be nice to make simple executable utilities that are self-contained with a UI that is easy to construct and that will just run on Windows without the hoops of forcing either JavaScript or Python into desktop programs.

I admit that I am a little surprised that threads are the recommended way (which feels like diving in the deep end) in a BASIC language to update the UI while doing something else in what is essentially a beginners program in the language.

That is not a complaint, but as I said it's just a surprise, and I am really satisfied with what I have achieved so far with the PureBasic IDE and language. It's a real pleasure to work in.

Re: How to update UI gadgets inside a procedure

Posted: Wed Nov 12, 2025 9:25 am
by Fred
This is how UI works, it processes the UI messages one by one on the main thread, so if you are doing something which locks the main thread, UI can't be updated. That's why if you have a real blocking operation (like compressing a big file etc.), you need to use thread. It's not something specific to PureBasic, it's the same in every other languages.

Re: How to update UI gadgets inside a procedure

Posted: Wed Nov 12, 2025 10:15 am
by susan
Fred wrote: Wed Nov 12, 2025 9:25 am This is how UI works
Understood!

Re: How to update UI gadgets inside a procedure

Posted: Wed Nov 12, 2025 11:42 am
by BarryG
susan wrote: Mon Nov 10, 2025 10:26 amI can't figure out how to make the SetGadgetText be shown before the loop begins, and before the procedure ends.
This works just fine for me (and it assumes your app doesn't need to do anything else at the time):

Code: Select all

Procedure DoLongProcedure()
  SetGadgetText(0, "Please wait...")
  DisableGadget(1,1)
  For index = 0 To 50000000
    ; doing something that takes many seconds
  Next
  SetGadgetText(0, "(Awaiting start)")
  DisableGadget(1,0)
EndProcedure

OpenWindow(0,300,300,300,80,"test",#PB_Window_SystemMenu)
TextGadget(0,10,10,280,25,"(Awaiting start)")
ButtonGadget(1,10,40,280,25,"Click here to start")
Repeat
  ev=WaitWindowEvent()
  If ev=#PB_Event_Gadget And EventGadget()=1
    DoLongProcedure()
  EndIf
Until ev=#PB_Event_CloseWindow

Re: How to update UI gadgets inside a procedure

Posted: Wed Nov 12, 2025 6:15 pm
by TI-994A
susan wrote: Wed Nov 12, 2025 3:25 am...I am a little surprised that threads are the recommended way (which feels like diving in the deep end) in a BASIC language to update the UI while doing something else in what is essentially a beginners program in the language.
Hi @susan. What you should be (pleasantly) surprised about is to find a BASIC language that supports threading so easily.

Here's a simple example which uses a timer to demonstrate the unblocked functionality of the main event loop and UI when a thread is running.

Code: Select all

; create your own custom event
#PB_MyProcedureIsDone = #PB_Event_FirstCustomValue

Procedure slowProcedure(dummy)
  For i = 0 To 300000000
    Pow(27.0, 1/3.0)
  Next
  PostEvent(#PB_MyProcedureIsDone)
EndProcedure

OpenWindow(0, 0, 0, 300, 200, "Simple Thread Example", 
           #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
TextGadget(0, 10, 10, 280, 30, "", #PB_Text_Center)
ButtonGadget(1, 10, 160, 280, 30, "Start Procedure")

Repeat
  event = WaitWindowEvent()
  Select event
    Case #PB_Event_CloseWindow
      appQuit = #True
    Case #PB_Event_Timer
      If EventTimer() = 0
        SetGadgetText(0, "Procedure running for " + timer + " seconds...")
        timer + 1
      EndIf             
    Case #PB_Event_Gadget
      If EventGadget() = 1
        timer = 1
        DisableGadget(1, #True)
        AddWindowTimer(0, 0, 1000)
        CreateThread(@slowProcedure(), 0)
      EndIf
    Case #PB_MyProcedureIsDone
      DisableGadget(1, #False)
      RemoveWindowTimer(0, 0)
      SetGadgetText(0, "Procedure completed in " + timer + " seconds!")
  EndSelect       
Until appQuit

And here's an expanded example to further demonstrate how to stop a thread safely and elegantly. Please note that although there is a function to kill threads, aptly named KillThread(), it's not recommended.

Code: Select all

; create your own custom event
#PB_MyProcedureIsDone = #PB_Event_FirstCustomValue

Global killThreadSwitch = #False

Procedure slowProcedure(dummy)
  For i = 0 To 300000000
    Pow(27.0, 1/3.0)
    If killThreadSwitch
      Break
    EndIf    
  Next
  If Not killThreadSwitch
    PostEvent(#PB_MyProcedureIsDone)
  EndIf  
EndProcedure

OpenWindow(0, 0, 0, 300, 200, "Simple Thread Example", 
           #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
TextGadget(0, 10, 10, 280, 30, "", #PB_Text_Center)
ButtonGadget(1, 10, 120, 280, 30, "Start Procedure")
ButtonGadget(2, 10, 160, 280, 30, "Force Stop Procedure")
DisableGadget(2, #True)

Repeat
  event = WaitWindowEvent()
  Select event
    Case #PB_Event_CloseWindow
      appQuit = #True
    Case #PB_Event_Timer
      If EventTimer() = 0
        SetGadgetText(0, "Procedure running for " + timer + " seconds.")
        timer + 1
      EndIf             
    Case #PB_Event_Gadget
      Select EventGadget()
        Case 1
          timer = 1
          killThreadSwitch = #False
          DisableGadget(1, #True)
          DisableGadget(2, #False)
          AddWindowTimer(0, 0, 1000)
          CreateThread(@slowProcedure(), 0)
        Case 2
          killThreadSwitch = #True
          DisableGadget(1, #False)
          DisableGadget(2, #True)
          RemoveWindowTimer(0, 0)
          SetGadgetText(0, "Procedure killed after " + timer + " seconds.")
      EndSelect      
    Case #PB_MyProcedureIsDone
      DisableGadget(1, #False)
      DisableGadget(2, #True)
      RemoveWindowTimer(0, 0)
      SetGadgetText(0, "Procedure completed in " + timer + " seconds.")
  EndSelect       
Until appQuit

Hope it helps. :D