Programming Windows in Haskell、次の一歩。

もうかれこれ半年以上前のポストの続きになるんですが、なんというか、HaskellでWin32を直にたたくスタイルでGUIアプリを書いてみたわけなんですが、前回挫折したのは、ウィンドウごとのステートをどうやって保持するかというところだったわけです。IORefを作ってその中にステートを書き込んだり読み出したりする方法をHaskellCafeなどでも見かけたりしたのですが、個人的にはなんともしっくり来ていなかったわけです。

先日久しぶりにこの辺のことをごにょごにょといじくってみる気になったので、コードをさわっていたわけです。HaskellでWin32アプリを書いたとき一番最初に僕が試したのはウィンドウのステートをShowしてSetWindowText/GetWindowText経由で保存するという技でした。これをやったときはまだIORefなんていうものも知らず、とにかく何か動かしたいと思ってやったことでした…

そして、次に出てきたのがIORef,Win32の常套手段ではSetWindowLong/GetWindowLongとかあるわけなんですが、なんか、やる気をそそられるものがなかったんですね…

今回、久々に気がむいて手を出してみたのはメッセージポンプの再帰呼び出しを利用して、変化するステートを維持するというモデルでした。スケッチとしては:

pump st = do
	(st', _) <- wndProc st
	pump st'
	
wndProc :: ST->HWND->MSG->WPARAM->LPARAM->
 IO(ST,LRESULT)

なんてものを夢見ていたわけです。でも、無理やり動かしてみると、これはもうWin32のプログラミングのルールを破りまくりで、いろんな形でおかしな動きをしてしまうわけです。

しかし、これをやって気づいたのは、ウィンドウプロシジャにひとつパラメタを追加してやったことで、そのウィンドウに独自の情報をCreateWindowじかに渡すことができることです。あとは、これがうまく変化させられれば良いわけなんです…
ぼーっとGraphics.Win32.Window.createWindowのコードを眺めていると、なぜか、このインプリメンテーションはAPIにユーザー指定のウィンドウプロシジャを渡さずに、その直後にsetWindowClosureという関数でウィンドウプロシジャを指定しています…不思議ですね…

setWindowClosureって、Haskellの関数をWin32のコールバック関数として登録できるのねーとおもって眺めていたら、ちょっと思いついちゃったわけです。といっても、そんな大それたことではないんですけど。

それはつまり、ウィンドウプロシジャ内でウィンドウのステートが変わるたんびに
  setWindowClosure wndProc st'
とやってやれば、そのウィンドウの状態をうまく管理できそうだということです。

それでやってみたのがこんなのです:

module Main
	where

import System.Win32.Types
import System.Win32.DLL
import Graphics.Win32
import Graphics.Win32.Misc
import Graphics.Win32.GDI.Types 
import Graphics.Win32.Message
import Graphics.Win32.Window
import Data.List (unfoldr)
import Data.Int (Int32)
import Data.Maybe
import Control.Monad
import Control.Arrow

foreign import stdcall "PostQuitMessage" 
   postQuitMessage :: Int32 -> IO ()


type Vector = [Float]
type Matrix = [ [Float] ]

transform :: Matrix -> Vector -> Vector
transform mx v = [ (mul r) v| r <- mx]
 where
  mul rs cs = sum $ zipWith ( * ) rs cs

mxRotXZ :: Float -> Matrix
mxRotXZ rot = [ [cos rot, 0, -1 * (sin rot) ],
  [0,       1,             0 ],
 [sin rot, 0,       cos rot ] ]

proj :: Vector -> Vector
proj v@(x: y: z: [ ]) = [x * fac, y * fac]
 where
  fac = (z - zvp) / (zvf - zvp)
  zvp = -4 :: Float
  zvf = -1 :: Float

cube :: [Vector]
cube = 	[ [1,  1,  1], [1, -1,  1], [-1, -1,  1], [-1,  1,  1],
  [1,  1, -1], [1, -1, -1], [-1, -1, -1], [-1,  1, -1],
  [1,  1,  1], [1,  1, -1], [-1,  1, -1], [-1,  1,  1],
  [1, -1,  1], [1, -1, -1], [-1, -1, -1], [-1, -1,  1] ]

toMaybe :: Bool -> a -> Maybe a
toMaybe p a
 | p = Just a
 | otherwise = Nothing

varMap :: ( [a] -> (b, [a] ) ) -> [a] -> [b]
varMap f a = unfoldr (uncurry toMaybe . (not.null &&& f) ) a

main = do
 let clsName 	=  mkClassName "My Window Class"
 hinst 		<- getModuleHandle Nothing
 whiteBrush	<- getStockBrush wHITE_BRUSH
 curArrow	<- loadCursor Nothing iDC_ARROW
 mAtom 		<- registerClass
  (cS_DBLCLKS, 
  hinst,		-- HINSTANCE
  Nothing,	-- Maybe HICON
  Just curArrow,	-- Maybe HCURSOR
  Just whiteBrush,-- Maybe HBRUSH
  Nothing,	-- Maybe LPCTSTR
  clsName)

 when (isJust mAtom) $ do
  hwnd <- createWindow 
   clsName
   "Rotating Box Demo" 
   (wS_THICKFRAME + wS_CAPTION + wS_SYSMENU) 
   Nothing 
   Nothing 
   Nothing 
   Nothing 
   Nothing 
   Nothing 
   hinst
   (wndProc 0)

  setWinTimer hwnd 0{-tid-} 50{-msec-}

  showWindow hwnd sW_SHOWNORMAL
  updateWindow hwnd
  allocaMessage $ pump

  unregisterClass clsName hinst

pump lpmsg = do
 fContinue <- getMessage lpmsg Nothing
 when fContinue $ do
  translateMessage lpmsg
  dispatchMessage lpmsg
  pump lpmsg

drawBox4 :: HDC -> [ [Int32] ] -> IO ()
drawBox4 hdc [ [x1, y1], [x2, y2], [x3, y3], [x4, y4] ] = do
 moveToEx hdc x1 y1
 lineTo   hdc x2 y2
 lineTo   hdc x3 y3
 lineTo   hdc x4 y4
 lineTo   hdc x1 y1

drawCube :: HDC -> Matrix -> IO ()
drawCube hdc rot = mapM_ (drawBox4 hdc) $ 
 varMap (splitAt 4) $ 
 map ( 
  (map $ ( + 400).round.( * 100) ) . proj . transform rot)
  cube

render :: HWND -> HDC -> Int -> IO ()
render hwnd hdc i  = (drawCube hdc . rot . fromIntegral) i
 where
  rot :: Float -> Matrix
  rot x = mxRotXZ $ (pi * x/ 180) 

onTimer :: HWND -> Int -> IO ()
onTimer hwnd i = do
  invalidateRect (Just hwnd) Nothing True
  setWindowClosure hwnd (wndProc $ (i + 2) `mod` 360) 

wndProc :: Int -> HWND -> WindowMessage -> WPARAM -> 
  LPARAM -> IO LRESULT
wndProc i hwnd wm wp lp
  | wm == wM_KEYDOWN 	= doFinish
  | wm == wM_LBUTTONDOWN 	= doFinish
  | wm == wM_DESTROY 	= killTimer (Just hwnd) 0 >> 
               postQuitMessage 0 >> return 0
  | wm == wM_SIZE	= doInvalRender
  | wm == wM_PAINT   	= onPaint
  | wm == wM_TIMER	= onTimer hwnd i >> return 0
  | otherwise 	   = defWindowProc (Just hwnd) wm wp lp
  where
    doFinish = sendMessage hwnd wM_CLOSE 1 0 >> return 0
    doInvalRender = do
         invalidateRect (Just hwnd) Nothing True
         return 0
    onPaint 	= allocaPAINTSTRUCT $ \ lpps -> do
     hdc <- beginPaint hwnd lpps
     render hwnd hdc i
     endPaint hwnd lpps
     return 0

昔、Doukakuのお題にあわせてやった簡易3Dアプリなんですが、今回の肝の部分は
wndProcの第一パラメタが(簡単すぎるという話はありますが)ウィンドウの状態を保持しているパラメタ、(回転する箱の角度)そしてそのパラメタがonTimerの中でsetWindowClosureでアップデートされています。

カリー化を使ってステートを管理できるとはなんともHaskellらしくていいかなと思ったわけです。よけいなIORefもいらないし…

ではでは。