A “Progress Bar” in AppGraphics

A user on our forums posted a question about creating a progress bar using AppGraphics. Because Fortran is often relied upon for long-running computations, a graphical progress bar could be very useful. While AppGraphics doesn’t provide a progress bar solution out of the box, creating one isn’t particularly difficult.

In it’s simplest form, a progress bar is really just a partially filled rectangle. With a little creativity, we can create a window that provides all the progress bar behaviors we might want. For maximum portability, we’ll assume that we want a standalone window that can display progress. Ideally, it should also provide:

  1. An optional “Cancel” button
  2. A way to display text
  3. A simple 0 to 100 percent update mechanism

For fun, we’ll also design our progress bar window to be Fortran-2003-esque using some type-bound procedures. To start, we need a derived type to describe our progress bar:

    type progresswindow

        integer::win
        integer::cancel
        
        character(128)::text
        
        contains
        
        procedure :: close => closeprogress
        procedure :: draw  => drawprogress
        
    end type progresswindow

The type includes integers that refer to our progress window, a possible cancel button, and the text we’re displaying above the progress bar. We’ve also attached two type-bound procedures: a “close” subroutine and a “draw” subroutine.

To initialize this window, we’ll basically just need to create the window and set up some drawing styles. An abbreviated summary is below:

        p%win = initwindow(400, 100, title=title, dbflag=.FALSE., closeflag=.FALSE.)
        call setbkcolor(systemcolor(COLOR_WINDOW_BKGD))
        call setcolor(BLACK)
        call settextstyle(WINDOWS_FONT, HORIZ_DIR, 10)
        call setfillstyle(SOLID_FILL, RED)

The above code creates a window, configures some colors and fonts to match Windows defaults, and sets up the fill style for a red progress bar.

The first type-bound procedure, close, is exceptionally simple, needing only to close this window, so we’ll skip that for now. In contrast, drawing the window is a bit more involved. We’ll need to take a number of steps. Our type definition pointed to a routine “drawprogress,” which we’ll define as such:

    subroutine drawprogress(p, completed, text)
    use appgraphics
    implicit none
    
        class(progresswindow), volatile::p
        integer, intent(in)::completed
        character(*), intent(in), optional::text

        ...

The drawing routine requires a completed percent, which is assumed to be 0 to 100, to be passed in. Optionally, the developer can also provide an updated text string to display above our progress bar. If the text is passed in, we’ll overwrite the text that our “progresswindow” variable is currently storing before we proceed with drawing.

First, we need to clear our viewport of existing graphics with an appropriate call:

    call setcurrentwindow(p%win)
    call clearviewport()

Note that prior to clearing the viewport, we’ve set the current AppGraphics window. This operation safeguards against applications where we may have multiple windows present. The developer, however, must be careful to switch windows back to any other windows when necessary.

Next, we can output the text into the window:

    if(len_trim(p%text) > 0) then
        call outtextxy(5, 5, trim(p%text))
    end if

If the text length is zero, we actually skip that step. Finally, we need to draw the progress bar. The actual progress bar really has two components: an empty rectangle and a filled rectangle whose size relative to the empty rectangle reflects the percent complete. AppGraphics doesn’t actually provide a quick routine for drawing unfilled rectangles, so we’ll instead use the relative line operations:

    progress_total = getmaxx() - 10
    
    ! Below we'll draw a simple box, unfilled
    call moveto(5, 25)
    call lineto(progress_total+5, 25)
    call lineto(progress_total+5, 50)
    call lineto(5, 50)
    call lineto(5, 25)

While the vertical coordinates aren’t particularly important, one should note that the empty rectangle should be about as wide as our window less five pixels on each side as a margin. Next, we can compute the width of filled rectangle based on what the developer passes into the drawing function:

    ! Bounds-check our completed value
    progress_completed = progress_total*completed/100
    if(progress_completed > progress_total) then
        progress_completed = progress_total
    elseif(progress_completed < 0) then
        progress_completed = 0
    end if

One additional step we’ve taken above is to ensure that the final width does not exceed the maximum width of our empty progress bar or that a negative with result from the user’s value. Once we have a width based on our completed progress, we can draw a filled bar:

    call bar(5, 25, progress_completed+5, 50)

This rather simple progress bar is actually quite effective. To test out how well it works, we can use a simplistic timing demo to watch the bar draw itself:

program main
use progress
implicit none

    type(progresswindow), volatile::p
    integer::counter
    real::last_time, now_time
    
    p = initprogress("Timed Progress")
    
    call cpu_time(last_time)
    now_time = last_time
    
    counter = 0
    
    do while(counter < 100) 
        
        do while(now_time - last_time < 0.2)
            call cpu_time(now_time)
        end do
        
        last_time = now_time
        counter = counter + 1
        
        call p%draw(counter)

    end do

    call p%close()

end program main

Because of our use of type-bound procedures, the actual driver routine is rather clean.

If you want to try out this simple progress bar, you can download the actual progress bar module and a slightly more advanced driver program (it implements a cancel button) below:


Creating Resizeable Windows with AppGraphics

With the release of Simply Fortran version 2.23, AppGraphics now supports resizeable windows. This feature greatly improves the functionality of the library, but there is a bit of a learning curve in creating and managing such windows. We’ll walk through a simple example below that should illustrate how to go about creating an application that uses resizeable windows.

The first step is proceed as normal in creating our window. Of course, we still need to specify an initial size:

myscreen = initwindow(320, 240, closeflag=.TRUE.)

In the above declaration, one may notice that there is no mention of a resizing capability. Wee need to use another call to enable resizing:

call enableresize(handle_resize)

The subroutine above will enable resizing for the current window. The argument we’ve included, handle_resize, is actually the name of a subroutine to be called whenever the window changes size. Such a subroutine would be useful if, for example, we need to reposition window controls or redraw graphic contents of a window. We’ll try to do both in this example.

In our main program, we’ll first add a button to our window in the lower right corner:

integer::mybutton
logical::quit
....
mybutton = createbutton(270, 210, 40, 20, "Close", handle_button)
quit = .FALSE.

In the code above, we’ve added a “Close” button that will, when clicked, call a subroutine handle_button that will flip the value of quit to .TRUE. and exit idle mode. We’ll also need an event loop in our code:

do while(.NOT. quit)
    call draw_window()
    call loop()
end do

call closewindow(myscreen)

The event loop above basically draws our window and enters idle mode. If we ever idle mode, it first checks if the quit flag was triggered and, if not, redraws the window. The above structure means that we need only a simple subroutine for our resize callback handle_resize:

    subroutine handle_resize()
    use appgraphics
    implicit none
    
        call stopidle()
        
    end subroutine handle_resize

All the logic for laying out our window needs to reside in the draw_window subroutine. We’ll try to do two things in our drawing subroutine:

  1. Position mybutton in the lower right corner
  2. Output the window size, centered, in our window

For both of these tasks, we’ll need to know the current window size, which can easily be done with calls to getmaxx and getmaxy:

    integer::w,h
    ...
        w = getmaxx()
        h = getmaxy()

Based on the window size, we can actually reposition our button quite easily with a call to setbuttonposition:

        call setbuttonposition(mybutton, w-50, h-30, 40, 20)

The next task is to draw something in our window. We’ll simply write the window size near the top of the window, erasing any earlier text first:

        integer::tw
        character(100)::info
        ...
        write(info, '(A5,1X,I5,A1,I5)') "Size:", w, "x", h
    
        call setviewport(0, 0, w, 40, .FALSE.)
        call clearviewport()
        tw = textwidth(trim(info))
        call outtextxy( (w/2-tw/2), 5, trim(info))

The code above first writes the current window dimensions into the string info. Next, it defines and immediately clears a viewport where previous text would have existed. Finally, it outputs the text, centered in the window.

When you first run the program, you should see something like:

agr1

Unlike other AppGraphics windows, however, this one can be resize by dragging the borders.  Thanks to our design, things will continue to look nice at other sizes:

agr2

Working with a resizeable window is rather easy as long as you properly handle drawing the window when resize occurs.

If you’d like to try the code out for yourself, feel free to do so by clicking the link below:

It should work fine in Simply Fortran version 2.23 or higher.