Progress Bar

Provides user feedback and cancel option for long running plugins.

Requires: iuplua

Basic Template

Constructing a basic Progress Bar is very simple.  Essentially, we have to do two things – define the properties of the window containing the bar, and those of the bar itself.  For the window, it is common to remove the normal Windows close button and menu bar, define a suitable title, and set the size, as the IUP default is rather small.  For the bar, its default range value is from 0 to 1, and this is ideal for presenting the fraction of a process that has been completed.

The example script below creates the progress bar with a suitable title using the StartProgressBar function.  A simple loop counts up to 10, setting the position of the bar after each step.  The bar title is then changed, and the process repeated counting back down.  Finally, the bar is hidden once the process is completed.

A key point to remember when implementing a progress bar is that it will generally slow down the process you are monitoring, as it diverts processing to update the bar position.  The optimum balance between speed and feedback can be achieved with a suitable updating frequency.  For example, if a process has 100,000 steps, it is not sensible to update the progress bar after each one.  Updating every thousand steps would probably be perfectly adequate.

In this basic template, the progress bar is controlled via global variables that are accessible from anywhere in the script.  For most purposes, this is perfectly adequate, but if you want to squeeze out every last drop of performance (at the cost of increased script complexity) the following sections describe how to develop more sophisticated versions.

function StartProgressBar(BarTitle)
    gaugeProgress = iup.progressbar{expand='HORIZONTAL'}
    dlgProgress = iup.dialog{title = BarTitle, dialogframe = 'YES', border = 'YES', iup.vbox {gaugeProgress}}
    dlgProgress.rastersize = '400x70'
    dlgProgress.menubox = 'NO'  --  Remove Windows close button and menu.
    dlgProgress:showxy(iup.CENTER, iup.CENTER)  --  Put up Progress Display
    return dlgProgress
end

require('iuplua');
iup.SetGlobal('CUSTOMQUITMESSAGE','YES');

-- Display progress bar with initial title

StartProgressBar('Counting up...')

for i = 1, 10, 1 do
  gaugeProgress.value = i / 10
  fhSleep(500)
end

dlgProgress.title = 'Counting down...'

for i = 10, 1, -1 do
  gaugeProgress.value = i / 10
  fhSleep(500)
end

dlgProgress.visible = 'NO'

fhMessageBox('End of demo.', 'MB_OK')

Function Prototype Version

This version uses a Function Prototype Closure to encapsulate the Global variables of the Global Variable Version, making them Private and Local, which has several benefits:

  • The variables cannot be accidentally accessed outside the function.
  • The variables have the run time performance of Locals.
  • The variables persist between function calls just like Globals.

Nevertheless, optionally some parameters can be held externally in a Global variable, without any loss of performance.

Function Prototype Closures are explained by Programming in Lua: Chapter 6.1 Closures.

Also this version offers several enhancements:

  • Improved display of both percentage progress and elapsed time clock.
  • Faster execution by updating the display only when necessary instead of every Step.
  • Defences against accidental misuse such as missing parameters, etc.
--[[
@Title: Progress Bar (drop in)
@Author: Jane Taubman / Mike Tate
@LastUpdated: January 2013
@Description: Allows easy adding of a Progress Bar to any long running Plugin
]]

-- Progress Bar Function Prototype --
function NewProgressBar(tblGauge)

	local tblGauge	= tblGauge or {}				-- Optional table of external parameters
	local strFont	= tblGauge.Font		or nil			-- Font dialogue default is current font
	local strButton	= tblGauge.Button	or "255 0 0"		-- Button colour default is red
	local strBehind	= tblGauge.Behind	or "255 255 255"	-- Background colour default is white
	local intShowX	= tblGauge.ShowX	or iup.CENTER		-- Show window default position is central
	local intShowY	= tblGauge.ShowY	or iup.CENTER
	local intMax, intVal, intPercent, intStart, intDelta, intScale, strClock, isBarStop
	local lblText, barGauge, lblDelta, btnStop, dlgGauge

	local function doFocus()					-- Bring the Progress Bar window into Focus
		dlgGauge.bringfront="YES"				-- If used too often, inhibits other windows scroll bars, etc
	end -- local function doFocus

	local function doUpdate()					-- Update the Progress Gauge and the Delta % with clock
		barGauge.value = intVal
		lblDelta.title = string.format("%4d %%      %s ",math.floor(intPercent),strClock)
	end -- local function doUpdate

	local function doReset()					-- Reset all dialogue variables and Update display
		intVal		= 0					-- Current value of Progress Bar
		intPercent	= 0.01					-- Percentage of progress
		intStart	= os.time()				-- Start time of progress
		intDelta	= 0					-- Delta time of progress
		intScale	= math.ceil( intMax / 1000 )		-- Scale of percentage per second (this guess is corrected in Step function)
		strClock	= "00 : 00 : 00"			-- Clock delta time display
		isBarStop	= false					-- Stop button pressed signal
		doUpdate()
		doFocus()
	end -- local function doReset

	local tblProgressBar = {

		Start = function(strTitle,intMaximum)			-- Create & start Progress Bar window
			if not dlgGauge then
				strTitle = strTitle or ""		-- Dialogue and button title
				intMax = intMaximum or 100		-- Maximun range of Progress Bar, default is 100
				local strSize = tostring( math.max( 100, string.len(" Stop "..strTitle) * 8 ) ).."x30"	-- Adjust Stop button size to Title
				lblText  = iup.label	{ title=" ", expand="YES", alignment="ACENTER", tip="Progress Message" }
				barGauge = iup.progressbar { rastersize="400x30", value=0, max=intMax, tip="Progress Bar" }
				lblDelta = iup.label	{ title=" ", expand="YES", alignment="ACENTER", tip="Percentage and Elapsed Time" }
				btnStop  = iup.button	{ title=" Stop "..strTitle, rastersize=strSize, fgcolor=strButton, tip="Stop Progress Button", action=function() isBarStop = true end }	-- Signal Stop button pressed	return iup.CLOSE -- Often caused main GUI to close !!!
				dlgGauge = iup.dialog	{ title=strTitle.." Progress ", font=strFont, dialogframe="YES", background=strBehind,	-- Remove Windows minimize/maximize menu
								iup.vbox{ alignment="ACENTER", gap="10", margin="10x10",
									lblText,
									barGauge,
									lblDelta,
									btnStop,
								},
								move_cb = function(self,x,y) tblGauge.ShowX = x tblGauge.ShowY = y end,
								close_cb = btnStop.action,		-- Windows Close button = Stop button
							}
				dlgGauge:showxy(intShowX,intShowY)					-- Show the Progress Bar window
				doReset()								-- Reset the Progress Bar display
			end
		end,

		Message = function(strText)								-- Show the Progress Bar message
			if dlgGauge then lblText.title = strText end
		end,

		Step = function(intStep)								-- Step the Progress Bar forward
			if dlgGauge then
				intVal = intVal + ( intStep or 1 )					-- Default step is 1
				local intNew = math.ceil( intVal / intMax * 100 * intScale ) / intScale
				if intPercent ~= intNew then						-- Update progress once per percent or per second, whichever is smaller
					intPercent = math.max( 0.1, intNew )				-- Ensure percentage is greater than zero
					if intVal > intMax then intVal = intMax intPercent = 100 end	-- Ensure values do not exceed maximum
					intNew = os.difftime(os.time(),intStart)
					if intDelta < intNew then					-- Update clock of elapsed time
						intDelta = intNew
						intScale = math.ceil( intDelta / intPercent )		-- Scale of seconds per percentage step
						local intHour = math.floor( intDelta / 3600 )
						local intMins = math.floor( intDelta / 60 - intHour * 60 )
						local intSecs = intDelta - intMins * 60 - intHour * 3600
						strClock = string.format("%02d : %02d : %02d",intHour,intMins,intSecs)
					end
					doUpdate()							-- Update the Progress Bar display
				end
				iup.LoopStep()
			end
		end,

		Focus = function()
			if dlgGauge then doFocus() end							-- Bring the Progress Bar window to front
		end,

		Reset = function()									-- Reset the Progress Bar display
			if dlgGauge then doReset() end
		end,

		Stop = function()									-- Check if Stop button pressed
			iup.LoopStep()
			return isBarStop
		end,

		Close = function()									-- Close the Progress Bar window
			isBarStop = false
			if dlgGauge then dlgGauge:destroy() dlgGauge = nil end
		end,

	} -- end newProgressBar
	return tblProgressBar
end -- function NewProgressBar

Usage

Default Internal Parameters Example

	local ProgressBar = NewProgressBar()
	ProgressBar.Start( "My Task" )		-- Defaults to 100 steps maximum
	for intStep = 1, 100 do
		ProgressBar.Message( intStep .. " %" )
		fhSleep( 50, 40 )		-- Emulate performing the task 
		ProgressBar.Step()		-- Defaults to step by 1
		if ProgressBar.Stop() then
			break
		end
	end 
	ProgressBar.Close()

Advanced External Parameters Example

	local tblGauge = {			-- Pass parameters into Progress Bar function prototype
		Button	= "0 0 255",		-- Blue stop button
		ShowX	= 100,			-- Position window near top left
		ShowY	= 100,
		Font	= "Tahoma, Bold 10",	-- Tahoma, bold 10 point font
		}
	local ProgressBar = NewProgressBar(tblGauge)
	ProgressBar.Start( "My Task", 300 )	-- Set maximum range to 300
	for intStep = 1, 300 do
		ProgressBar.Message( "Step = " .. intStep )
		fhSleep( 20, 20 )		-- Emulate performing the task 
		ProgressBar.Step(1)
		if ProgressBar.Stop() then
			ProgressBar.Reset()
			fhSleep( 1500, 100 )	-- Emulate progress stopped action
			break
		end
	end 
	ProgressBar.Close()

Reducing Reverse Progress Bar Example

	local ProgressBar = NewProgressBar()
	ProgressBar.Start( "My Task", 100 )
	ProgressBar.Step(100)			-- Start with full bar
	for intStep = 1, 100 do
		ProgressBar.Message( 100 - intStep .. " %" )
		fhSleep( 50, 40 )		-- Emulate performing the task 
		ProgressBar.Step(-1)		-- Step backwards
		if ProgressBar.Stop() then
			break
		end
	end 
	ProgressBar.Close()

Global Variable Version

This version requires Global variables to communicate between the various functions.

--[[
@Title: Progress Display (drop in)
@Author: Jane Taubman / Mike Tate
@LastUpdated: May 2012
@Description: Allows easy adding of a Progress Bar to any long running Plugin
]]

StrWhite = "255 255 255"

ProgressDisplay = {

	Start = function(strTitle,intMax)	-- Create and start the Progress Display window controls
		if not dlgProgress then
			cancelflag = false
			local cancelbutton = iup.button{ title="Cancel", rastersize="200x30", 
				action = function()
					cancelflag = true -- Signal that Cancel button was pressed 
					return iup.CLOSE
				end
			}
			gaugeProgress	= iup.progressbar{ rastersize="400x30", max=intMax }		-- Set progress bar maximum range
			messageline	= iup.label{ title=" ", expand="YES", alignment="ACENTER" } 
			dlgProgress	= iup.dialog{ title=strTitle, dialogframe="YES", background=StrWhite,	-- Remove Windows minimize/maximize menu
				iup.vbox{ alignment="ACENTER", gap="10", margin="10x10",
					messageline,
					gaugeProgress,
					cancelbutton
				}
			}
			dlgProgress.close_cb = cancelbutton.action	-- Windows Close button acts as Cancel button
			dlgProgress:showxy(iup.CENTER, iup.CENTER)	-- Show the Progress Display dialogue window
		end
	end,

	SetMessage = function(strMessage)	-- Set the progress message
		if dlgProgress then messageline.title = strMessage end
	end,

	Step = function(iStep)			-- Step the Progress Bar forward
		if dlgProgress then
			gaugeProgress.value = gaugeProgress.value + iStep
			local val = tonumber(gaugeProgress.value)
			local max = tonumber(gaugeProgress.max)
			if val > max then
				gaugeProgress.value = 0
			end
			iup.LoopStep()
		end
	end,

	Reset = function()			-- Reset progress bar
		if dlgProgress then gaugeProgress.value = 0 end
	end,

	Cancel = function()			-- Check if Cancel button pressed
		return cancelflag
	end,

	Close = function()			-- Close the dialogue window
		cancelflag = false
		if dlgProgress then dlgProgress:destroy() dlgProgress = nil end
	end,

}

Usage

Note the iup.LoopStep() function may also be needed in the main loop of the plugin.

ProgressDisplay.Start('This is my progress box',100)
for i=1,100 do
	ProgressDisplay.SetMessage(i.." %")
	fhSleep(50,40)	-- Emulate performing the task 
	ProgressDisplay.Step(1)
	if ProgressDisplay.Cancel() then
		break
	end
end 
ProgressDisplay.Reset()
ProgressDisplay.Close()