Building a text editor (Part 1)
Edit: added cabal install information. Thanks Slaava!
This is the first post in a new series, aimed at wxHaskell beginners.
I did not write the code in this post. It was written by Fernando Benavides especially for this blog, and I’m extremely grateful for his help. The code is BSD3 licensed and under his copyright. You can fetch it as a cabal package (update: cabal install wxhnotepad), which is the best way to follow this tutorial. The fact that Fernando wrote the code means that in case of any disagreement between my commentary and the code, you should believe the code 🙂
I should also mention that Fernando is the author of λpage, which he describes as a Haskell scrapbook – you can think of it as a GUI-based and more featureful GHCi. It has several neat features: Hayoo integration and the ability to determine the kind of types are two stand-outs for me. I enjoyed checking it out, and find myself using it quite often. See what you think.
The first step in the tutorial is just about the most minimal wxHaskell application imaginable: a text editor without the ability to load or save text, no search and no replace. It’s a good start though, and in the next few posts it will develop into a usable notepad replacement.
Fernando has written the code in a particularly neat way – there are actually six versions of the editor, and you can call any of them from the main launcher screen. We’ll start with the launcher, since it is the main module for the program.
There are two libraries which make up wxHaskell:
- WXCore, a fairly thin wrapper around wxWidgets (the C++ library which does most of the rendering). The functions in WXCore tend to require a style of programming similar to C++, although we at least benefit from Haskell type inference when using them.
- WX, a higher-level library which enables a more declarative style of programming. As a general rule, you will probably want to use WX functions whenever possible and drop down to WXCore when WX does not do what you need. You will actually need to do this relatively often, as there are quite a few useful features in WXCore which do not have equivalents in WX.
At the top of any wxHaskell program, you will almost certainly want to import WXCore and WX, so:
> module Main where > > import Graphics.UI.WX > import Graphics.UI.WXCore > > import Step1 > import Step2 > import Step3 > import Step4 > import Step5 > import Step6
Like every other Haskell program, wxHaskell applications start with main.
In this case we have a very simple main program, but in a more complex application you might have come command line option handling, configuration file handling or similar. However, the essential part of a wxHaskell application is a call to start with a sequence of IO actions which make up the GUI logic of your program.
For wxHaskell application programming, that’s all you really need to know about start-up, but for the curious (or those who have done some wxWidgets programming in C++), what is happening under the hood is that start creates an instance of a wxHaskell application class derived from wxApp and this instance uses the closure represented by gui in the implementation of wxApp::onInit().
> main :: IO () > main = start gui
The launcher is very simple. It consists of a Frame which has a menu and nothing else.
Any wxHaskell application requires a top level container window. This is almost always a Frame, although it is possible to use a Dialog as a container if you have a very simple form-type GUI in mind. If you want a menu, you will definitely need a Frame, and we want a menu.
It is worth a closer look at how the Frame is created and configured, since this is a model for creating many windows and controls in wxHaskell. The wxHaskell documentation for frame says that is has type frame :: [Prop (Frame ())] -> IO (Frame ()), and that it creates a top level frame window.
What does this [Prop (Frame ())] mean? Well, wxHaskell has a mechanism which lets you perform much of the configuration of the different window and widget types in a declarative fashion by setting (or reading back) Attributes. This is much nicer than mucking about with lots of individual functions, and tends to create code which is clear and easy to understand.
The precise meaning of [Prop (Frame ())] is that you may provide a list of Properties which are applicable to a Frame. Most of these properties have been grouped into typeclasses, and the documentation usually indicates which are supported by a given type (the usually here should be taken to say that if you try this for Frame, you will be disappointed to find out that the documentation doesn’t mention this at all…)
- The text attribute is typically used to manipulate some form of meaningful text in a control. In the case of a Frame it sets the text in the window frame; in the case of a static text or text control it sets the text in the control.
- The visible attribute is supported by most controls – it determines whether the control is visible or not.
- The on attribute is more interesting. It is typically followed by an event name, and the pair is used to configure an event handler.
Thus, the short snippet of code below creates a Frame with the caption “wxhNotepad”, which is initially invisible and which calls a function, wxcAppExit when the Frame closing event is fired.
One other point of note: the set function can be used to set or change the properties of a window or control which has already been created (there is a related get function for reading values back as well). In this case we could have put the closing event handler into the property list we used when creating the frame, but in some cases (e.g. when you need to know the control identity to configure things correctly), you will need to use this
two (or more) phase approach.
> gui :: IO () > gui = > do > win <- frame [text := "wxhNotepad", visible := False] > set win [on closing := wxcAppExit]
The say function is a convenience for launching a small information dialog box.
> let say title desc = infoDialog win title desc
The main purpose of the launcher is to support the menu which lets you use the editor in each of its stages of development. There are a couple of ways of creating a menu (we’ll see the lower-level way later).
The simplest is to use menuPane to create a menu heading (e.g. ‘File’), menuItem to create an item in a menu (e.g. ‘Open’) and menuBar to set the menu for a frame. There are convenience functions for certain menu entries, notably the ‘Help’, ‘About’ and ‘Quit’ menu items, as these may require special handling on certain OS platforms, and using the convenience function ensures that you do this in the way expected on each OS.
It is worth quickly noting that for menu items, the text property is used for the menu text. Notice also that menu shortcuts and accelerators can be defined in the menu text itself. The wxWidgets documentation for wxMenu::Append() describes this format in detail.
> mnuSteps <- menuPane [text := "Steps"] > menuItem mnuSteps [on command := step1, > text := "Step &1 - Just a Text Field\tCtrl-1"] > menuItem mnuSteps [on command := step2, > text := "Step &2 - Open / Save / Save As...\tCtrl-2"] > menuItem mnuSteps [on command := step3, > text := "Step &3 - Undo / Redo...\tCtrl-3"] > menuItem mnuSteps [on command := step4, > text := "Step &4 - Cut / Copy / Paste...\tCtrl-4"] > menuItem mnuSteps [on command := step5, > text := "Step &5 - Find / Replace...\tCtrl-5"] > menuItem mnuSteps [on command := step6, > text := "Step &6 - Toolbar / Statusbar / Context Menus\tCtrl-6"] > menuQuit mnuSteps [on command := wxcAppExit] > > mnuHelp <- menuHelp  > menuAbout mnuHelp [on command := say "About wxHNotepad" > "Author: Fernando Brujo Benavides\nWebsite: http://github.com/elbrujohalcon/wxhnotepad"]
Once we have created a menu, it needs to be assigned to our Frame, and this is done with the menuBar property mentioned earlier. Notice that the menuBar takes a list of menuPane instances.
We finally make the Frame visible. There’s a neat trick to note here: what we are trying to do is to create a Frame which contains nothing but a menu. This is no problem on OSX, but Windows and Linux don’t really support it. However, if you set the clientSize property of the Frame to (0,0) then you get much the same effect.
> set win [menuBar := [mnuSteps, mnuHelp], > visible := True, clientSize := sz 0 0]
Text editor: step 1
This is the absolutely minimal, first stage text editor. It consists of nothing more than a text control inside a Frame. No menu. No dialogs and no real functionality. Perhaps the only aspect of interest is to note that
it is easy to make a wxHaskell application with more than one Frame, should you wish to.
Since this is the second time we have mentioned clientSize, it is probably worth a moment to explain what this means. There are several different ways to look at the size of a window. Two of the
most important are:
- clientSize The size available for displaying information inside a window – i.e. if the clientSize is (640, 480), this means that there are 640 x 480 (width x height) pixels available for displaying information. Borders, window decorations and so on take up additional space.
- virtualSize The size which would be needed to display all of the information contained in the window. Often the clientSize and virtualSize are the same, but if there is more information in the window than can be displayed fully (i.e. if you need a scroll bar) then the clientSize will be smaller than the virtualSize.
There is one new call in the snippet below: textCtrl constructs a text control. As parameters it requires a parent window (which in this case will be the Frame, win) and a list of properties. All non top-level windows require their parent as a parameter, since wxWidgets keeps windows in a hierarchy rooted at the top-level.
> step1 = > do > win <- frame [text := "wxhNotepad - Step 1", visible := False] > > editor <- textCtrl win [font := fontFixed, > text := "This is our first step in the " ++ > "developing of our text editor.\n" ++ > "Just a big text area where " ++ > "the user can read and write text\n" ++ > "That's not much, but it's just the " ++ > "beginning..."]
In the following snippet, we use the Layout mechanism to help us to fit our textCtrl inside the frame.
Layout is a conceptually elegant mechanism which allows you to describe the way in which the windows in an application are arranged and organized in a declarative manner.
The reality is that some aspects of the way that Layout works are rather complex and subtle (so much so that Layout will probably be the subject of a blog article or two of its own in the near future), but in this demonstration, Layout will work very well for us. I’d just ask that you don’t get discouraged if you have issues with more complex arrangements of controls using Layout.
Layout is specified using the layout property. It consists of a sequence of combinator functions which must ultimately resolve to a Attr w Layout type. For the moment, we just need to know that:
- widget requires a control as a parameter. It indicates how the specified control should be placed in the Layout.
- fill is a Layout transformer which indicates that the Layout to which it is applied should stretch and expand into the available area.
The documentation for Layout will give a better feel for what is happening, if you would like to understand more, but please bear in mind that this is a fairly complex subject, since what Layout is actually doing is to create sizer instances from the Layout specification, and sizers are a fairly large subject in themselves.
> set win [layout := fill $ widget editor, > clientSize := sz 640 480] > > focusOn editor > > set win [visible := True]