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:
LineEdit
for entering a filter that will limit what files are displayed in the files listLabel
for showing current directoryListBox
for 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.LineEdit
is 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.dir
and display it inFileManager.dirLabel
widget 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 updateListBox
with list of of files - when user changes a filter in
FileManager.fileFilterEdit
edit box, we limit displayed files to those that match the filter - when user presses
Esc
when 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 -u
https://github.com/akavel/rsrc
, which should download the source code, buildrsrc.exe
and copy it%GOPATH%\bin
, which should be in your%PATH%
. Ifgo get
fails, you can download the code locally withgit clone
https://github.com/akavel/rsrc.git
, build withgo build
and copyrsrc.exe
somewhere in the%PATH%
. - run
rsrc -manifest app.manifest -o rsrc.syso
to creatersrc.syso
file 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 failed
printed to the console.