Now that we have a user interface up and running, we need to load some real data and display it. We start this task by obtaining a list of image files for the requested directory and updating the user interface to list those instead of the placeholder information. Remember, at this stage, to add the extra image imports so we can decode all of the images that we will then filter for in a new getImageList() function:
import (
_ "image/jpeg"
_ "image/png"
_ "image/gif"
)
var names []string
func getImageList(dir string) []string {
files, _ := ioutil.ReadDir(dir)
for _, file := range files {
if file.IsDir() {
continue
}
ext := strings.ToLower(filepath.Ext(file.Name()))
if ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif" {
names = append(names, file.Name())
}
}
return names
}
The listing shows a fairly simple algorithm for checking each item in a directory and adding it to a names list if the filename looks like a image file that we support. Simple filename extension checking should be sufficient for our purposes here. We add these filenames to a global list for later use in the user interface.
Once we have a list of supported files, we can update the existing makeList() function. The new version iterates over the files list and adds a new cell for each item. The makeCell() function does not need any additional work to use this new content, but we do pass the array index for use later in the button handlers. We also save the images loaded in memory for display when selected:
var images []image.Image
func makeList(dir string, files []string) node.Node {
parent := makeCell(-1, filepath.Base(dir), nil)
children := []node.Node{parent}
for idx, name := range files {
img := loadImage(path.Join(dir, name))
cell := makeCell(idx, name, img)
children = append(children, cell)
images = append(images, img)
}
return widget.NewFlow(widget.AxisVertical, children...)
}
To update the main image displayed, we need to add a new function to our scaledImage widget. This new SetImage() function sets the image reference to be displayed and marks the widget for painting. Updating the node.MarkNeedsPaintBase mark means that the widget will be repainted next time there is a graphical paint event (we will discuss paint events in more detail shortly):
func (w *scaledImage) SetImage(img image.Image) {
w.Src = img
w.Mark(node.MarkNeedsPaintBase)
}
To make use of this new function, we update our chooseImage() code to set the image selected. We also need to store a reference to the scaledImage widget created to call the function on:
var view *scaledImage
func chooseImage(idx int) {
view.SetImage(images[idx])
}
When the image is changed, we also need to set the correct filename to the label above the image. To do so, we will add a reference to the widget.Label object and set its Text field. After updating this property, we also need to set the node.MarkNeedsMeasureLayout flag, as the text may have a different size to the previous content. We use the names array and the index variable passed into chooseImage() to look up the content. This could also be accomplished by creating a list of items using a new object type that stores the image, name, and metadata in a single list, but the approach of multiple indexed lists is easier to explain in smaller code samples:
var name *widget.Label
var index = 0
func chooseImage(idx int) {
index = idx
view.SetImage(images[idx])
name.Text = names[idx]
name.Mark(node.MarkNeedsMeasureLayout)
name.Mark(node.MarkNeedsPaintBase)
}
We also need to fill in the empty previousImage() and nextImage() functions that the header buttons call. A simple helper function called changeImage() is added to handle image switching based on an offset from the current image (either 1 or -1). Each button callback calls this with the appropriate offset:
func changeImage(offset int) {
newidx := index + offset
if newidx < 0 || newidx >= len(images) {
return
}
chooseImage(newidx)
}
func previousImage() {
changeImage(-1)
}
func nextImage() {
changeImage(1)
}
With this in place, the main() function can include a call to chooseImage(0) to load the first image found in the directory. Of course, you should check that there is at least one image before you do this.
The last change is to determine which directory to show images for when the application loads. The previous main() function is renamed loadUI() (which takes a directory parameter to pass into getImageList() and makeList()). A new main function is created that parses command-line arguments to allow the user to specify a directory. The following code will print out a helpful usage hint if some unexpected parameters are passed (or if --help is specified) and if no parameters are found, it will show the current working directory (using os.Getwd()):
func main() {
dir, _ := os.Getwd()
flag.Usage = func() {
fmt.Println("goimages takes a single, optional, directory parameter")
}
flag.Parse()
if len(flag.Args()) > 1 {
flag.Usage()
os.Exit(2)
} else if len(flag.Args()) == 1 {
dir = flag.Args()[0]
if _, err := ioutil.ReadDir(dir); os.IsNotExist(err) {
fmt.Println("Directory", dir, "does not exist or could not be read")
os.Exit(1)
}
}
loadUI(dir)
}
With these modifications, we've created a complete image viewer application that displays thumbnails for a whole directory of images and one large image view. By tapping on items in the list, or using the Next and Previous buttons, you can switch between the images available. While this works, it can be quite slow to load in a large directory. Next, we'll explore how this can be improved: