3

My tools involved here are GTK and Haskell. My questions are probably pretty trivial for anyone who has done significant GUI work, but I've been off in the equivalent of CGI applications for my whole career.

I'm building an application that displays tabular data, displays the same data in a graph form, and has an edit field for both entering new data and for editing existing data. After asking about sharing resources, I decided that all of the data involved will be stored in an MVar so that every component can just read the current state from the MVar.

All of that works, but now it is time for me to rearrange the application so that it can be interactive.

With that in mind, I have three widgets: a TextView (for editing), a TreeView (for displaying the data), and a DrawingArea (for displaying the data as a graph).

I THINK I need to do two things, and the core of my question is, are these the right things, or is there a better way.

Thing the first: All event handlers, those functions that will be called any time a redisplay is needed, need to be written at a high level and then passed into the function that actually constructs the widget to begin with. For instance:

drawStatData :: DrawingArea -> MVar Core.ST -> (Core.ST -> SetRepWorkout.WorkoutStore) -> IO ()
createStatView :: (DrawingArea -> IO ()) -> IO VBox

createUI :: MVar Core.ST -> (Core.ST -> SetRepWorkout.WorkoutStore) -> IO HBox
createUI storeMVar field = do
    graphs <- createStatView (\area -> drawStatData area storeMVar field)

    hbox <- hBoxNew False 10
    boxPackStart hbox graphs PackNatural 0

    return hbox

In this case, createStatView builds up a VBox that contains a DrawingArea to graph the data and potentially other widgets. It attaches drawStatData to the realize and exposeEvent events for the DrawingArea. I would do something similar for the TreeView, but I am not completely sure what since I have not yet done it and what I am thinking of would involve replacing the TreeModel every time the TreeView needs to be updated.

My alternative to the above would be...

drawStatData :: DrawingArea -> MVar Core.ST -> (Core.ST -> SetRepWorkout.WorkoutStore) -> IO ()
createStatView :: IO (VBox, DrawingArea)

... but in this case, I would arrange createUI like so:

createUI :: MVar Core.ST -> (Core.ST -> SetRepWorkout.WorkoutStore) -> IO HBox
createUI storeMVar field = do
    (graphbox, graph) <- createStatView (\area -> drawStatData area storeMVar field)

    hbox <- hBoxNew False 10
    boxPackStart hbox graphs PackNatural 0

    on graph realize (drawStatData graph storeMVar field)
    on graph exposeEvent (do liftIO $ drawStatData graph storeMVar field
                             return ())

    return hbox

I'm not sure which is better, but that does lead me to...

Thing the second: it will be necessary for me to rig up an event system so that various events can send signals all the way to my widgets. I'm going to need a mediator of some kind to pass events around and to translate application-semantic events to the actual events that my widgets respond to. Is it better for me to pass my addressable widgets up the call stack to the level where the mediator lives, or to pass the mediator down the call stack and have the widgets register directly with it?

So, in summary, my two questions:

1) pass widgets up the call stack to a global mediator, or pass the global mediator down and have the widgets register themselves to it? 2) pass my redraw functions to the builders and have the builders attach the redraw functions to the constructed widgets, or pass the constructed widgets back and have a higher level attach the redraw functions (and potentially link some widgets together)?

Okay, and...

3) Books or wikis about GUI application architecture, preferably coherent architectures where people aren't arguing about minute details?


The application in its current form (displays data but does not write data or allow for much interaction) is available at https://bitbucket.org/savannidgerinel/fitness . You can run the application by going to the root directory and typing

runhaskell -isrc src/Main.hs data/

or...

cabal build
dist/build/fitness/fitness data/

You may need to install libraries, but cabal should tell you which ones.

1 Answer 1

1

After a couple of weeks of work, I have what I consider a satisfactory solution which I think I can ultimately refactor into a Good solution.

I decided that there are two types of events in the system. My basic widgets; the TreeView, DrawingArea, TextView, Submit button, and Clear button; are one logical grouping. That particular grouping can (and in my application, is) be duplicated. Interacting with most of them provides a set of GUI events, but those GUI events are only important to other widgets in the grouping. From that perspective, when I construct the widgets, I wire all of their events together. Those events never leave the widget set.

Additionally, I have a set of semantic events. So far, the events amount to "Workout Submitted" and "Workout Updated". The "Workout Submitted" event is fired off when the submit button is pressed and the data in the text field parses successfully. The higher-level application processes that event, updates the application state, and then fires off the "Workout Updated" event to indicate to the entire application that a particular workout got updated.

The upshot of all of this seems relatively straightforward:

  • Create an application-global dispatcher that deals with events that have meaning in the application domain
  • If a group of widgets are meant to work together as a whole, construct them together. Bind together all of the GUI events in which they affect one another but not the application. Hand them a way to register for application events and a way to fire application events.
  • Make the event handlers take as few parameters as possible, ideally taking only the exact data that has been updated or that needs to be passed around. Auxiliary data for how to process it should be bound to the handler in the section of code in which the handler is defined. Partial application is a fantastic thing here, but in C you could use an opaque data structure.

whew This did not take very long to figure out, but figuring out how to actually hack the code took quite a few hours.

I'm going to do a much more in-depth write-up on my website, including significant code samples, some time in the future.

1
  • I'm now facing the same exact problem, and came up with a similar solution. I'd like to read more about it, if you managed to write a follow-up on your website.
    – Jose Faeti
    Oct 30, 2015 at 7:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.