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

Scroll bars

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

4 thoughts on “Custom Controls in wxHaskell (part 2)”

  1. First, let me congratulate you on taking this initiative – it should be well worthwile. I have had some thoughts about what support wxHaskell newbies need to
    get going.

    First a suggestion – how about a screenshot of the finished control ? It would help
    visualize what is being put together.

    Now, I’d like to pick up on some general aspects of what you wrote.

    In Part 1 you said “The control I am building is subclassed from Panel. This is probably a good choice for most controls as it can contain a top level sizer and many children.”
    In Part 2 (Creating the child windows) you say “these use the Window type as we are going to take responsibility for painting this part of the control;”

    The issue I have with wxHaskell is not having clear documentation about what
    widgets do what and how they should be put together. For example I had a scroll-bar
    bug that I posted on sourceforge – ultimately I discovered that setting a “layout”
    attribute for the scrolled window caused the strange behaviour, which disappeared if I used a “size” attribute. So the big question is, where is this documented?

    The wxWidgets website doesn’t seem to help and neither did the wxWidget C++ book.

  2. Hi Andrew. Thanks for being first commenter!

    I’ll sort out a screenshot later today – I agree that it would help.

    To be honest, the wider API documentation isn’t all that it could be. In particular, I have never managed to really get my head around the Layout type (wxHaskell was originally written by Daan Leijen, who is a far more proficient Haskell designer than I am).

    I believe there are a few subtle bugs in Layout, and I’m starting to work my way through this part of the code in far more detail in the hope of understanding and documenting it more thoroughly.

    I’ll try to do a posting about the basic anatomy of an application, however. This is a good idea.

  3. Seems like it may be useful to also present the high-level layout-based version of buildLayout even if wxHaskell does the wrong thing with it in practice. The idea would be “this is the code we /would/ write…”

    It’d be a shame for people to glance at this and walk away with the impression that you have to write that sort of low-level code in wxHaskell.

Leave a reply to Andrew Butterfield Cancel reply