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 w开发者_运维百科ant 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.
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.
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!
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.
精彩评论