How to pass state between event handlers in gtk2hs

2020-05-24 05:14发布

问题:

I'm trying to make a toy application, just to get my head around how to write event driven programs in Haskell. What I'm trying to do is draw a line to a canvas which moves forward every time a key is pressed (so it's sort of a primordial cursor in a text editor).

My problem is I can't work out what the best way to count the number of times the user has pressed a key. Obviously I can't use a global variable like I would in an imperative program so presumably I need to pass the state around on the call stack, but in GTK execution descends into the main loop after each event handler returns and since I don't control the main loop I don't see how I can pass the changed global state from one event handler. So how can one event handler ever pass state onto another event handler?

I have a sort of partial solution here, where the keyboard event re-curries myDraw and sets it as a new the event handler. I'm not sure if this solution can be extended, or even if it's a good idea.

What's the best particle solution to this problem?

import Graphics.UI.Gtk
import Graphics.Rendering.Cairo

main :: IO ()
main= do
     initGUI
     window <- windowNew
     set window [windowTitle := "Hello World",
                 windowDefaultWidth := 300, windowDefaultHeight := 200]

     canvas <- drawingAreaNew
     containerAdd window canvas

     widgetShowAll window 
     draWin <- widgetGetDrawWindow canvas
     canvas `on` exposeEvent $ do liftIO $ renderWithDrawable draWin (myDraw 10)
                                  return False

     window `on` keyPressEvent $ onKeyboard canvas
     window `on` destroyEvent  $ do liftIO mainQuit
                                    return False

     mainGUI

onKeyboard :: DrawingArea -> EventM EKey Bool
onKeyboard canvas = do 
  liftIO $ do drawWin <- widgetGetDrawWindow canvas
              canvas `on` exposeEvent $ do liftIO $renderWithDrawable drawWin (myDraw 20)
                                           return False
              widgetQueueDraw canvas
  return False



myDraw :: Double -> Render ()
myDraw pos = do
    setSourceRGB 1 1 1
    paint
    setSourceRGB 0 0 0

    moveTo pos 0
    lineTo pos 20
    stroke 

回答1:

First off, you could have a global. Ignoring that solution as bad form, this looks like a job for an MVar. In main you just make a new MVar which you can update in onKeyboard and check in myDraw:

...
import Control.Concurrent.MVar

main = do
    ...
    mv <- newMVar 0
    ....
    canvas `on` exposeEvent $ do liftIO $ renderWithDrawable draWin (myDraw mv 10)
    ...
    window `on` keyPressEvent $ onKeyboard canvas mv

onKeyboard canvas mv = do
    modifyMVar_ mv (\x -> return (x + 1))
    ....

myDraw mv pos = do
    val <- readMVar mv
    ...

Note that sharing mutable state is also often useful when passing functions as arguments by first using partial application to provide the MVar (or TVar, IORef, whatever).

Oh, and a warning: MVars aren't strict - if the application has the potential to write lots without forcing the value (i.e. reading and comparing the contained Int), then you should force the value before writing to avoid building a huge thunk.