Custom Controls in wxHaskell (part 2)
It may save you some typing to know that once this series is complete, I shall be publishing a Cabalized version of the Diff control on Hackage.
Subclassing the control
Witness types are used to represent the class hierarchy of the underlying wxWidgets library. The idea is that, for example, a Panel, which is a descendent of Window, can use all functions which accept a Window type.
Define diffViewer as a subclass of Window.
> type DiffViewer a = Panel (CDiffViewer a) > data CDiffViewer a = CDiffViewer
Creating the child windows
Create an instance of a diffViewer control.
In this case we create a panel as a child of the provided parent window, and set its style flags to indicate that the control will expand (horizontally and vertically) into the space allocated to it. We use the cast operator to convert the created panel and its properties into a DiffViewer related types. The cast operator is very dangerous, and should be used with great care (this is pretty much the only time you should need it in your wxHaskell life) – it operates essentially like the C cast operator!
> diffViewer :: Window a -> [Prop (DiffViewer ())] -> IO (DiffViewer ()) > diffViewer parent props = > do > p <- panel parent [style := wxEXPAND] > let dv = cast p > dvprops = castProps cast props > diffViewer' dv dvprops > where > cast :: Panel a -> DiffViewer () > cast = objectCast
The diffViewer’ function does most of the work of creating and configuring the child windows in the control. These are all children of the panel, which is treated as the ‘owner’ of the control, and is the only window whose identity need be made visible outside of the control implementation.
A few things to note:
- the diff output will be displayed on windows f1 and f2 – these use the Window type as we are going to take responsibility for painting this part of the control;
- we create and manage scroll bars manually – this is because we wish to use the same scroll bars to scroll both of the windows containing diff information;
- we are constructing a layout manually from sizers (call to buildLayout).
> diffViewer' p props = > do > fn1 <- staticText p [clientSize := sz 400 (-1)] > fn2 <- staticText p [clientSize := sz 400 (-1)] > f1 <- window p  > f2 <- window p  > vsb <- scrollBarCreate p (-1) rectNull wxVERTICAL > hsb <- scrollBarCreate p (-1) rectNull wxHORIZONTAL > set f1 [ on paint := onPaint p DVorig f1 ] > set f2 [ on paint := onPaint p DVchanged f2 ] > let state = DVS p fn1 fn2 f1 f2 vsb hsb Nothing dvf_default Map.empty > defaults = [border := BorderStatic] > scrollBarSetEventHandler hsb (onScroll p hsb) > scrollBarSetEventHandler vsb (onScroll p vsb) > dvSetState p state > set p (defaults ++ props) > buildLayout p fn1 fn2 f1 f2 vsb hsb > return p
Laying out the child windows
The wxHaskell layout implementation is buggy in some circumstances (it doesn’t seem to handle resizes as I would expect when window size exceeds minsize). Since we want the control to follow the size hints given by the owning application, we will use sizers to create a manual layout. In theory, wxHaskell layout should behave identically, but it doesn’t – that’s a bug to go and look for another day…
Since wxHaskell was originally designed to abstract the creation of sizers using layout, this code is rather low level, using functions from WXCore – you would be forgiven for thinking that it is just C++ implemented in Haskell, and that is essentially exactly what it is – most of the functions in WXCore are Haskell wrappers around the wxWidgets C++ API, which has the benefit that you can use the wxWidgets C++ API documentation to help to understand what most WXCore functions do.
The last two lines: the calls to windowLayout and windowFit are critical, and should be called before the application which uses the Diff control performs its own layout (they set the constraints for size which the application should respect when setting the size of its own windows).
> buildLayout p fn1 fn2 f1 f2 vsb hsb = > boxSizerCreate wxVERTICAL >>= \p_sizer -> > boxSizerCreate wxHORIZONTAL >>= \h_sizer -> > boxSizerCreate wxVERTICAL >>= \l_sizer -> > boxSizerCreate wxVERTICAL >>= \r_sizer -> > sizerAddWindow l_sizer fn1 0 (wxALL .|. wxEXPAND) 5 nullPtr >> > sizerAddWindow l_sizer f1 1 wxEXPAND 10 nullPtr >> > sizerAddSizer h_sizer l_sizer 1 wxEXPAND 5 nullPtr >> > sizerAddWindow r_sizer fn2 0 (wxALL .|. wxEXPAND) 5 nullPtr >> > sizerAddWindow r_sizer f2 1 wxEXPAND 10 nullPtr >> > sizerAddSizer h_sizer r_sizer 1 wxEXPAND 5 nullPtr >> > sizerAddWindow h_sizer vsb 0 (wxALL .|. wxEXPAND) 5 nullPtr >> > sizerAddSizer p_sizer h_sizer 1 wxEXPAND 5 nullPtr >> > sizerAddWindow p_sizer hsb 0 (wxALL .|. wxEXPAND) 5 nullPtr >> > windowSetSizer p p_sizer >> > windowLayout p >> > windowFit p
In many cases, very little handling is required for scroll bars in wxHaskell since many of the common controls contain all the handling required for most purposes. However, as mentioned earlier, we are going to use a single set of control bars to control two client windows (one will contain the ‘original’ text and the other will contain the ‘updated’ text).
This code should serve as a simple example of custom scroll bar handling in wxHaskell.
Here we configure a custom event handler which provides the same handling for all scroll bar events. In more demanding applications (e.g. where the wxEVT_SCROLL_THUMBTRACK would cause too much processing to give smooth operation), you may want to do something a little different, perhaps by defining separate event handlers for different scroll bar events.
> scrollBarSetEventHandler window evtHandler = > windowOnEvent window events evtHandler (\evt -> evtHandler) > where > events = [ wxEVT_SCROLL_BOTTOM > , wxEVT_SCROLL_LINEDOWN > , wxEVT_SCROLL_LINEUP > , wxEVT_SCROLL_PAGEDOWN > , wxEVT_SCROLL_PAGEUP > , wxEVT_SCROLL_THUMBRELEASE > , wxEVT_SCROLL_THUMBTRACK > , wxEVT_SCROLL_TOP ]
Define the ‘on scroll’ event handler for the scroll bars. In this case we can live with using the same event handler for both vertical and horizontal scroll bars as we will be updating the entire client area of the controlled windows on each scroll event (this is not too onerous, at least on my machine). This means that we just need to inform the parent window to refresh (i.e. repaint) the entire window next time the UI gets a chance to do so.
> onScroll dv _ = > windowRefresh dv True