Have you ever written an app that needs to do some heavy lifting, like processing a big batch of files? Maybe you click a button, and suddenly your entire app just … stops. The cursor turns into a spinning wheel, the windows won’t respond, and your users are left wondering if it crashed. It’s a frustrating experience, but don’t worry, there’s a solution: Threads!
Let’s build a simple but incredibly powerful Desktop app: a threaded image resizer. This tool will let you pick a folder of images and resize them all without ever locking up the app. It’s a perfect showcase of how easy and effective threading can be in Xojo.
Let’s get started!
Step 1: Designing the User Interface
First things first, let’s lay out our app’s window. We’re going for a clean and simple design. All you need to do is create a new Xojo Desktop project and add the following controls from the Library onto your main window:
- DesktopButton: This will be our “start” button. The user will click this to select a folder and begin the resizing process. Let’s set its
Caption
to “Select folder && Start”. - DesktopProgressBar: This will give the user visual feedback on the progress of the resizing operation.
- DesktopLabel: We’ll use this label to display status updates, like which file is currently being processed or when the job is complete. Let’s call it
StatusLabel
.
Your app layout should look something like this:

Step 2: Adding the Magic Ingredient: The Thread
Now for the star of the show! Go to the Library and find the Thread
object. Drag and drop one onto your window. By default, it might be named Thread1
, but I’ve renamed mine to ResizeThread
to make its purpose crystal clear. This object is where our background work will happen.

Quick note on thread types: Cooperative threads (the default) share a single CPU core with the main/UI thread and other cooperative threads. Preemptive threads can run on separate CPU cores and deliver true parallelism for CPU-bound work. More info on this subject: Cooperative to Preemptive: Weaving New Threads into your Apps
Why this project uses Preemptive threads: resizing many images is CPU-bound and benefits from parallel cores without blocking the UI.
Step 3: Kicking Off the Process
With the UI and thread in place, let’s add the code to get things moving. Double-click the “Select folder & Start” button to add its Pressed
event. This is what will run when the user clicks it.
Here’s the code for the Pressed
event:
' Ask the user for a source folder (contains images)
Var f As FolderItem = FolderItem.ShowSelectFolderDialog
If f = Nil Then Return ' user cancelled
mSourceFolder = f
' Reset UI
ProgressBar1.Value = 0
StatusLabel.Text = "Starting…"
' Kick off background work!
ResizeThread.Start
Let’s break this down. First, we ask the user to select a folder. If they don’t pick one, we simply exit. If they do, we store the selected folder in a window property called mSourceFolder
(Public Property mSourceFolder As FolderItem
). We then reset our progress bar and status label and, most importantly, we call ResizeThread.Start
. That one simple line tells our thread to wake up and get to work by running its Run
event.
Step 4: The Heavy Lifting (in the Background)
The Run
event of the ResizeThread
is where the core logic lives. This is where we’ll find, load, resize, and save the images. Remember, the golden rule of threads is: never touch the UI directly from the Run event. Doing so can cause crashes and unpredictable behavior.
Instead, we perform our task and then send a message back to the main thread with an update. We do this using a method called AddUserInterfaceUpdate
.
Here’s the Run
event code:
' Quick validation up-front
If mSourceFolder = Nil Or Not mSourceFolder.Exists Or Not mSourceFolder.IsFolder Then
Me.AddUserInterfaceUpdate(New Dictionary("progress":0, "msg":"No folder selected"))
Return
End If
Try
' Ensure output subfolder "<selected>/resized" exists
Var outFolder As FolderItem = mSourceFolder.Child("resized")
If Not outFolder.Exists Then outFolder.CreateFolder
' Build a list of candidate image files.
' Note: Using Picture.Open to validate images is simple and robust.
' (For very large folders, you could pre-filter by extension first.)
Var images() As FolderItem
For Each it As FolderItem In mSourceFolder.Children
If it = Nil Or it.IsFolder Then Continue ' Ignore subfolders
Try
Var p As Picture = Picture.Open(it)
If p <> Nil Then images.Add(it)
Catch e As RuntimeException
' Non-image or unreadable; skip
End Try
Next
Var total As Integer = images.Count
If total = 0 Then
Me.AddUserInterfaceUpdate(New Dictionary("progress":0, "msg":"No images found"))
Return
End If
' Resize settings (simple “fit within 800x800” bounding box)
Const kMaxW As Double = 800.0
Const kMaxH As Double = 800.0
' Short delay after each file so the main thread can repaint the UI
Const kDelayMS As Integer = 50
For i As Integer = 0 To total - 1
Var src As FolderItem = images(i)
Try
' Load source (immutable)
Var pic As Picture = Picture.Open(src)
If pic = Nil Or pic.Width <= 0 Or pic.Height <= 0 Then Continue
' Compute proportional scale (never upscale)
Var sW As Double = kMaxW / pic.Width
Var sH As Double = kMaxH / pic.Height
Var scale As Double = Min(Min(sW, sH), 1.0)
Var newW As Integer = Max(1, pic.Width * scale)
Var newH As Integer = Max(1, pic.Height * scale)
' Render into a new mutable bitmap of the target size
Var outPic As New Picture(newW, newH)
outPic.Graphics.DrawPicture(pic, 0, 0, newW, newH, 0, 0, pic.Width, pic.Height)
' Build a safe base name (strip the last extension; handle dotfiles)
Var name As String = src.Name
Var ext As String = name.LastField(".")
Var baseName As String
If ext = name Then
' No dot in the name
baseName = name
Else
baseName = name.Left(name.Length - ext.Length - 1)
End If
If baseName.Trim = "" Then baseName = "image"
' Save JPEG (fallback PNG if JPEG export not supported)
Var outFile As FolderItem = outFolder.Child(baseName + "_resized.jpg")
If Picture.IsExportFormatSupported(Picture.Formats.JPEG) Then
outPic.Save(outFile, Picture.Formats.JPEG, Picture.QualityHigh) ' adjust the quality of the jpeg here
Else
outPic.Save(outFolder.Child(baseName + "_resized.png"), Picture.Formats.PNG)
End If
Catch io As IOException
' Likely a write/permission/disk issue; skip this file
Catch u As UnsupportedOperationException
' Unsupported format/operation on this platform; skip
End Try
' Progress + message to the UI (safe handoff)
Var pct As Integer = ((i + 1) * 100) / total
Me.AddUserInterfaceUpdate(New Dictionary("progress":pct, _
"msg":"Resized " + src.Name + " (" + pct.ToString + "%)"))
' Let the main thread paint the update
Me.Sleep(kDelayMS, True) ' wakeEarly=True allows early resume when idle
Next
' Final UI message
Me.AddUserInterfaceUpdate(New Dictionary("progress":100, "msg":"Done"))
Catch e As RuntimeException
' Any unexpected failure: report a friendly error
Me.AddUserInterfaceUpdate(New Dictionary("progress":0, "msg":"Error: " + e.Message))
End Try
Step 5: Receiving Updates and Safely Changing the UI
So, how does the main thread “hear” these updates? Through the UserInterfaceUpdate
event on the ResizeThread
itself! This event fires on the main thread, making it the one and only safe place to update our controls.
Here’s the code for the UserInterfaceUpdate
event:
' This event runs on the main thread – SAFE to update controls here.
If data.Count = 0 Then Return
Var latest As Dictionary = data(data.LastIndex)
If latest.HasKey("progress") Then ProgressBar1.Value = latest.Value("progress")
If latest.HasKey("msg") Then StatusLabel.Text = latest.Value("msg")
' A little bonus: when we are done, open the output folder!
If latest.HasKey("msg") And latest.Value("msg") = "Done" Then
Var outFolder As FolderItem = mSourceFolder.Child("resized")
If outFolder <> Nil And outFolder.Exists Then
outFolder.Open
End If
End If
In this event, we receive an array of Dictionaries (all the updates that have queued up). We usually only care about the very latest one, so we grab data(data.LastIndex)
. Then, we safely access the values from the dictionary and assign them to ProgressBar1.Value
and StatusLabel.Text
. That’s it!
Conclusion
And there you have it! A fully functional, non-blocking image resizer. By moving the heavy work to a Thread
, we’ve created an application that provides a smooth, professional user experience.
The pattern is simple:
- Start the task from the main thread (
ResizeThread.Start
). - Work inside the thread’s
Run
event. - Communicate back to the UI with
AddUserInterfaceUpdate
. - Update the UI safely in the
UserInterfaceUpdate
event.
That’s pretty much it! Go ahead, download this project’s source code from GitHub, play around with it, and think about how you can use threads in your own applications!
Happy coding!
Gabriel is a digital marketing enthusiast who loves coding with Xojo to create cool software tools for any platform. He is always eager to learn and share new ideas!