wxhaskell asynchronous updates

2019-05-11 08:21发布

问题:

I am using WxHaskell to graphically show the state of a program that advertises state updates using TCP (which I decode using Data.Binary). When an update is received, I want to update the display. So I want the GUI to update its display asynchronously. I know that processExecAsync runs a command line process asynchronously, but I don't think this is what I want.

回答1:

This is rough code using transactional variables (i.e. software transactional memory). You could use an IORef, MVar, or numerous other constructs.

main = do
    recvFunc <- initNetwork
    cntTV <- newTVarIO 0
    forkIO $ threadA recvFunc cntTV
    runGUI cntTV 0

Above you start the program, initialize the network and a shared variable cntTV

threadA recvCntFromNetwork cntTVar = forever $ do
    cnt <- recvCntFromNetwork
    atomically (writeTVar cntTVar cnt)

threadA receives data from the network and writes the new value of the counter to the shared variable.

runGUI cntTVar currentCnt = do
    counter <- initGUI
    cnt <- atomically $ do
        cnt <- readTVar cntTVar
        if (cnt == currentCnt)
            then retry
            else return cnt
    updateGUICounter counter cnt
    runGUI cntTVar cnt

runGUI reads the shared variable and if there is a change will update the GUI counter. FYI, the runGUI thread won't wake up on retry until cntTVar is modified, so this isn't a CPU hogging polling loop.

In this code I've assumed you have functions named updateGUICounter, initGUI, and initNetwork. I advise you use Hoogle to find the location of any other functions you don't already know and learn a little about each module.



回答2:

I have come up with a kind of hack that seems to work. Namely, use an event timer to check an update queue:

startClient :: IO (TVar [Update])
startClient = /*Connect to server, 
                listen for updates and add to queue*/

gui :: TVar [Update] -> IO ()
gui trdl = do
  f <- frame [text := "counter", visible := False]
  p <- panel f []
  st <- staticText p []
  t <- timer f [interval := 10, on command := updateGui st]
  set f [layout := container p $ fill $ widget st, clientSize := (sz 200 100), visible := True]
 where
   updateGui st = do
             rdl <- atomically $ readTVar trdl
             atomically $ writeTVar trdl []
             case rdl of
               [] -> return ()
               dat : dl -> set st [text := (show dat)]

main :: IO ()
main = startClient >>= start gui

So a client listens for the updates on the TCP connection, adds them to a queue. Every 10ms, an event is raised whose action is to check this queue and show the latest update in a static text widget.

If you have a better solution, please let me know!



回答3:

I found a solution without a busy wait at: http://snipplr.com/view/17538/

However you might choose a higher eventId in order to avoid conflicts with existing ids.

Here is some code from my module http://code.haskell.org/alsa/gui/src/Common.hs:

myEventId :: Int
myEventId = WXCore.wxID_HIGHEST+100
    -- the custom event ID, avoid clash with Graphics.UI.WXCore.Types.varTopId

-- | the custom event is registered as a menu event
createMyEvent :: IO (WXCore.CommandEvent ())
createMyEvent =
   WXCore.commandEventCreate WXCore.wxEVT_COMMAND_MENU_SELECTED myEventId

registerMyEvent :: WXCore.EvtHandler a -> IO () -> IO ()
registerMyEvent win io =
   WXCore.evtHandlerOnMenuCommand win myEventId io


reactOnEvent, reactOnEventTimer ::
   SndSeq.AllowInput mode =>
   Int -> WX.Window a -> Sequencer mode ->
   (Event.T -> IO ()) ->
   IO ()
reactOnEvent _interval frame (Sequencer h _) action = do
   mvar <- MVar.newEmptyMVar

   void $ forkIO $ forever $ do
      MVar.putMVar mvar =<< Event.input h
      WXCore.evtHandlerAddPendingEvent frame =<< createMyEvent

   registerMyEvent frame $
      MVar.takeMVar mvar >>= action

-- naive implementation using a timer, requires Non-Blocking sequencer mode
reactOnEventTimer interval frame sequ action =
   void $
   WX.timer frame [
      WX.interval := interval,
      on command  := getWaitingEvents sequ >>= mapM_ action]

The code shows two ways to handle the problem:

  • reactOnEventTimer does a busy wait using the WX timer.
  • reactOnEvent only gets active when an event actually arrives. This is the prefered solution.

In my example I wait for ALSA MIDI sequencer messages. The Event.input call waits for the next ALSA message to come. The action gets the results of Event.input, that is, the incoming ALSA messages, but it is run in the WX thread.