A first window app
suggest changeLet's build a simple app that shows a list of files in a list view and has a search box for filtering results:
Full source is here with the most relevant code in file_manager.go.
The app is the simplest file manager. It shows the files in the directory, allows navigating file system and limiting files shown to only those matching text entered in the edit control.
Showing all files in directory:
Showing only files matching go:
Defining window layout
Let's define FileManager struct responsible for managing of the window:
// FileManager manages file manager window
type FileManager struct {
window *walk.MainWindow
fileFilterEdit *walk.LineEdit
dirLabel *walk.Label
filesListBox *walk.ListBox
// ... more fields
}
The window consists of 3 widgets, arranged in a vertical list:
LineEditfor entering a filter that will limit what files are displayed in the files listLabelfor showing current directoryListBoxfor showing list of files and directories in a current directory
Walk has 2 layers:
- widgets that are wrappers around win32 Windows controls e.g.
walk.LineEditis a wrapper around Windows edit control. - an optional declarative system for easily defining the layout of a Window
Here's a declarative layout of the above window:
var fm FileManager
def := declarative.MainWindow{
AssignTo: &fm.window,
Title: "File Manageer",
MinSize: declarative.Size{Width: 240, Height: 200},
Size: declarative.Size{Width: 320, Height: 400},
Layout: declarative.VBox{},
Children: []declarative.Widget{
declarative.LineEdit{
AssignTo: &fm.fileFilterEdit,
Visible: true,
CueBanner: "Enter file filter",
OnTextChanged: fm.onFilterChanged,
OnKeyUp: func(key walk.Key) {
if key == walk.KeyEscape {
fm.fileFilterEdit.SetText("")
}
},
},
declarative.Label{
AssignTo: &fm.dirLabel,
Visible: true,
Text: "Directory:",
},
declarative.ListBox{
AssignTo: &fm.filesListBox,
Visible: true,
OnItemActivated: fm.onFileClicked,
},
},
}
Each widget has a corresponding declarative definition e.g. declarative.LineEdit corresponds to walk.LineEdit.
A declarative.MainWindow corresponds to top-level window.
Some widgets, like MainWindow can have children (which are also widgets). For those widgets Layout specifies how children are arranged inside the parent. We use a VBox layout which arranges widgets in a vertical list.
Creating a window
To create window and its widgets we call:
err := def.Create()
if err != nil {
return nil, err
}
This creates actual widgets based on declarative definition. During creation we can set initial properties on the widget and provide handlers for events generated by widgets.
AssignTo will set a given variable to a created widget, which allows us to manipulate the real widget in the future.
Visible allows to set the initial visibility state of the widget.
Text specifies initial text of Label widget.
CueBanner is a text displayed in LineEdit when its text is empty and the control doesn't have focus.
OnTextChanged is an event handler that will be called when text in LineEdit changes.
OnItemActivated is an event handler that will be called when user double-clicks on a list item.
Running event loop
Windows GUI is event based. After we create a window, we need to run event loop which ensures processing of window messages:
_ = fm.window.Run()
Event loop finishes when the user closes the window.
Further execution of program logic happens as a result of responding to events generated by the user interacting with the widgets.
Thread-safe access to widget via synchronization
On Windows only the thread that created a given window / control can safely operate on this window / control. In most cases it's the main thread of the application, called UI thread.
Walk inherits this limitation. Calling any method on a widget needs to be done on UI thread. If the code runs in a goroutine and manipulates widgets, it must be executed on the right thread. Use widget.Synchronize(fn) to schedule arbitrary function to be executed on UI thread.
Crashes are often a result of not observing that rule.
Handling events
Some widgets broadcast events based on user interaction. We can provide functions to be called when an event happens.
LineEdit widget broadcasts OnTextChanged event when a text in edit widget changes. In our application we retrieve the text and use it to filter list of files to only show those that match the text:
func (fm *FileManager) onFilterChanged() {
filter := fm.fileFilterEdit.Text()
fm.applyFileFilter(filter)
}
ListBox widget broadcasts OnItemActivated event when a list item is activated e.g. with mouse double-click. We can retrieve the item selected with:
idx := fm.filesListBox.CurrentIndex()
In our case we use it to navigate the directory hierarchy when user double-clicks a directory name.
Setting the content of a label
A Label is a simple widget that displays a line of text, which we can set with:
fm.dirLabel.SetText(s)
Setting the content of ListBox
Some widgets, like ListBox or TreeView show more complicated content like a list or tree of items.
For ListBox the content is abstracted as ListModel. It's an interface that you implement to provide information about the list data.
Walk handles simple cases for us. For example we can use a slice of strings []string{} as a model:
model := []string{"item1", "item2"}
listBox.SetModel(model)
Application logic
You can study the rest of the code to see how to tie different pieces together.
The high-level description is:
- we remember the name of the current directory in
FileManager.dirand display it inFileManager.dirLabelwidget of typewalk.Label - when user double-clicks name of directory in the
ListBox, we get a list of files / directories in that directory on a goroutine (so that we don't block the UI). When that is finished weSynchronize()the code to updateListBoxwith list of of files - when user changes a filter in
FileManager.fileFilterEditedit box, we limit displayed files to those that match the filter - when user presses
Escwhen infileFilterEdit, we clear the file filter. It's one of those little touches that make software more enjoyable for users.
Compiling as a windows application
Compiling the program as a valid Windows application is a bit more involved than a regular Go program.
For modern Windows applications you need a manifest. You can re-use this manifest for most apps.
In short it declares that the application is compatible with all Windows versions and that it opts in to modern look of controls by asking for version 6 of common controls library.
A manifest must be embedded into an .exe as a resource.
To achieve that:
- install rsrc tool by e.g. doing
go get -uhttps://github.com/akavel/rsrc, which should download the source code, buildrsrc.exeand copy it%GOPATH%\bin, which should be in your%PATH%. Ifgo getfails, you can download the code locally withgit clonehttps://github.com/akavel/rsrc.git, build withgo buildand copyrsrc.exesomewhere in the%PATH%. - run
rsrc -manifest app.manifest -o rsrc.sysoto creatersrc.sysofile that the go linker will embed as a resource - run
go build -ldflags="-H windowsgui" -o app.exe .
The argument ldflags="-H windowsgui" to go build passes -H windowsgui flag to the linker.
This flag marks executable as a Windows GUI application (as opposed to the default console application).
If you don't embed the manifest in the application, it might fail to start in one of two ways:
- If you compiled as a modern Windows app (i.e., with
-H windowsgui), the app will fail silently. - If you compiled as a non-Windows app (i.e., without
-H windowsgui), you might get an errorTTM_ADDTOOL failedprinted to the console.