iTranslated by AI
Displaying a Window with Haskell and the Win32 API
This is an article for Day 24 of the Haskell Advent Calendar 2025.
You can call the Win32 API from Haskell by using the System.Win32 module in GHC. In this article, I will try to display a simple window using this System.Win32 module.
Overview of the steps for simple window display
Before trying to display a window using System.Win32 in Haskell, let's review the flow of a program that displays a simple window written in C. The flow is as follows:
#include <windows.h>
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE _, LPSTR _, int nCmdShow) {
WNDCLASS wc = {0};
wc.lpfnWndProc = DefWindowProc;
wc.hInstance = hInst;
wc.lpszClassName = "MyWndClass";
RegisterClass(&wc);
HWND hwnd = CreateWindow(
wc.lpszClassName,
"Simple Window",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 300, 200,
NULL, NULL, hInst, NULL
);
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
The procedure is as follows:
- Prepare data for the
WNDCLASSstructure - Register the window class with the
RegisterClassfunction - Create a window with the
CreateWindowfunction - Display the window with the
ShowWindowfunction - Request the initial redraw with the
UpdateWindowfunction - Continue to retrieve window messages in a loop with the
GetMessagefunction- Exit the loop when the termination window message (
WM_QUIT) is received - Send character input events from key input events with the
TranslateMessagefunction (unnecessary if using only physical key input events) - Send the retrieved window events to the window procedure with the
DispatchMessagefunction
- Exit the loop when the termination window message (
We will now replace this with a program written in Haskell using the System.Win32 package.
Preparing WNDCLASS data
On the Haskell side, the WNDCLASS structure is defined in the Graphics.Win32.Window module as a type alias for a tuple type with seven elements, as shown below:
type WNDCLASS =
(ClassStyle, -- style
HINSTANCE, -- hInstance
Maybe HICON, -- hIcon
Maybe HCURSOR, -- hCursor
Maybe HBRUSH, -- hbrBackground
Maybe LPCTSTR, -- lpszMenuName
ClassName) -- lpszClassName
Optional items are wrapped in Maybe, so you can pass Nothing when omitting them.
Getting the instance handle
Since you cannot define a WinMain function in Haskell, you must use the getModuleHandle function as an alternative way to obtain an instance handle.
The getModuleHandle function is defined in the System.Win32.DLL module.
getModuleHandle :: Maybe String -> IO HMODULE
The first argument specifies the module name (executable file name), but if you specify Nothing, the handle for the executable file of the current process is returned.
HMODULE is defined as:
type HMODULE = Ptr ()
where Ptr is a type constructor defined in the Foreign.Ptr module for FFI.
As a test, let's try running a program that simply displays the value of the module handle obtained with getModuleHandle.
import System.Win32.DLL (getModuleHandle)
main :: IO ()
main = do
hModule <- getModuleHandle Nothing
print hModule
0x00007ff790150000
The value will vary depending on the environment and situation, but we were able to obtain a value.
While the type of the instance handle passed to WNDCLASS is HINSTANCE and the type obtained from getModuleHandle is HMODULE, both are aliases for Ptr (), so there is no problem.
type HINSTANCE = Ptr ()
type HMODULE = Ptr ()
ClassStyle
ClassStyle is an alias for UINT, and you can specify window behaviors using bit flags. However, since we are omitting them this time, we will specify 0.
ClassName
The class name is an identifier for the window class used when calling CreateWindow. Any suitable name is fine for now, but its type is ClassName.
The definition of the ClassName type is as follows:
type ClassName = LPCTSTR
which goes like:
type LPCTSTR = LPTSTR
type LPTSTR = Ptr TCHAR
type TCHAR = CWchar
CWchar is a type from Foreign.C.Types. I won't go any deeper than this, but it seems that you can create a ClassName from a String using the mkClassName function in Graphics.Win32.Window.
mkClassName :: String -> ClassName
WNDCLASS
As a result, we will set the WNDCLASS value for this instance as follows:
hModule <- getModuleHandle Nothing
let wndClass =
( 0,
hModule,
Nothing,
Nothing,
Nothing,
Nothing,
mkClassName "SimpleWindow"
)
Registering the Window Class
The RegisterClass function is defined as the registerClass function on the Haskell side, with the following type signature:
registerClass :: WNDCLASS -> IO (Maybe ATOM)
The ATOM type is defined in the System.Win32.Types module as:
type ATOM = WORD
type WORD = Word16
Word16 is a type from the standard Haskell Data.Word module.
This ATOM value is a value that uniquely identifies the registered class, but we did not use this value in the C example.
Now, pass the WNDCLASS value prepared earlier to the registerClass function.
import Graphics.Win32.Window (mkClassName, registerClass)
import System.Win32.DLL (getModuleHandle)
main :: IO ()
main = do
hModule <- getModuleHandle Nothing
let wndClass =
( 0,
hModule,
Nothing,
Nothing,
Nothing,
Nothing,
mkClassName "SimpleWindow"
)
registerClass wndClass
return ()
This completes the registration of the window class.
Since we have only registered the window class, running this program will result in nothing happening, and the program will simply terminate.
Creating a Window
Create a window using the createWindow function from Graphics.Win32.Window.
createWindow
:: ClassName -> String -> WindowStyle ->
Maybe Pos -> Maybe Pos -> Maybe Pos -> Maybe Pos ->
Maybe HWND -> Maybe HMENU -> HINSTANCE -> WindowClosure ->
IO HWND
The last argument is WindowClosure, which does not exist in the Win32 API's CreateWindow function.
Originally, the window procedure is specified in the WNDCLASS structure. However, on the Haskell side, it seems the window procedure is specified via this createWindow call instead of WNDCLASS, and that is what WindowClosure is for.
Since the original Win32 API CreateWindow function has no argument corresponding to WindowClosure, there might be some internal reasons for why specifying a function here allows it to be called as the window procedure. For now, it's enough to know that the WindowClosure specified here will be called as the window procedure.
This WindowClosure is not a Maybe, so you must pass a function even if the application does nothing.
The type of WindowClosure is as follows:
type WindowClosure = HWND -> WindowMessage -> WPARAM -> LPARAM -> IO LRESULT
In the C sample provided earlier, DefWindowProc was specified directly as the window procedure to handle the window's standard behavior. Haskell also provides a defWindowProc function, but its first argument Maybe HWND differs from the HWND in WindowClosure.
defWindowProc :: Maybe HWND -> WindowMessage -> WPARAM -> LPARAM -> IO LRESULT
Therefore, to create a window procedure that only performs standard actions, you need to wrap HWND in Just.
windowClosure :: WindowClosure
windowClosure hWnd = defWindowProc (Just hWnd)
With this in mind, call the createWindow function.
hWnd <-
createWindow
className
"Window Title"
wS_OVERLAPPEDWINDOW
Nothing
Nothing
Nothing
Nothing
Nothing
Nothing
hModule
windowClosure
Now the window itself can be created, but running this will still not display the window.
Displaying the window
To display the window, use the showWindow function.
showWindow :: HWND -> ShowWindowControl -> IO Bool
For ShowWindowControl, sW_SHOWNORMAL is sufficient for now.
showWindow hWnd sW_SHOWNORMAL
To have the drawing within the window executed immediately, call the updateWindow function.
updateWindow :: HWND -> IO ()
updateWindow hWnd
At this point, the window will be displayed, but the program will terminate immediately, and the window will close in an instant.
Message loop
To prevent the program from terminating immediately, you need to implement a message loop.
First, prepare a function for the message loop and allocate memory to receive messages.
To allocate memory for receiving messages, use the allocaMessage function. This seems to be unique to Haskell's Win32 package rather than being part of the Win32 API itself.
allocaMessage :: forall a. (LPMSG -> IO a) -> IO a
LPMSG is a pointer to the memory where message information will be stored.
messageLoop :: IO ()
messageLoop = allocaMessage $ \msg -> messageLoop
Next, retrieve the window message. Use the getMessage function to get the window message.
getMessage :: LPMSG -> Maybe HWND -> IO Bool
messageLoop :: IO ()
messageLoop = allocaMessage $ \msg -> do
getMessage msg Nothing
messageLoop
When a program termination message (WM_QUIT) is received as a window message, the getMessage function returns False; otherwise, it returns True. Therefore, we will ensure that the message loop continues only when True is received.
import Control.Monad (when)
messageLoop :: IO ()
messageLoop = allocaMessage $ \msg -> do
continue <- getMessage msg Nothing
when continue $ do
messageLoop
Use the dispatchMessage function to send the retrieved message to the window procedure.
dispatchMessage :: LPMSG -> IO LONG
messageLoop :: IO ()
messageLoop = allocaMessage $ \msg -> do
continue <- getMessage msg Nothing
when continue $ do
dispatchMessage msg
messageLoop
While not strictly required this time, call the translateMessage function to convert key input messages into character input messages.
messageLoop :: IO ()
messageLoop = allocaMessage $ \msg -> do
continue <- getMessage msg Nothing
when continue $ do
translateMessage msg
dispatchMessage msg
messageLoop
Calling messageLoop at the end of the main function starts the message loop. When you run this program, the window will be displayed.

A simple window is displayed
Terminating the program when the window is closed
If the window procedure remains as defWindowProc, the program will not terminate when the window is closed (when the "Close" button is clicked). Therefore, we need to ensure the program terminates when the window is closed.
The program can be terminated by exiting the message loop, but the message loop was set up to continue as long as getMessage returns True.
messageLoop = allocaMessage $ \msg -> do
continue <- getMessage msg Nothing
when continue $ do
-- Omitted
By calling the postQuitMessage function, you can make getMessage return False.
postQuitMessage :: Int -> IO ()
The postQuitMessage function takes one Int argument, which represents the exit code. If there are no specific issues, 0 is fine.
postQuitMessage 0 -- Terminates the message loop
When the window is closed, a window message called wM_DESTROY is sent. We capture this message in the window procedure and call postQuitMessage there.
windowClosure hWnd msg w l
| msg == wM_DESTROY =
postQuitMessage 0
return 0
The type of windowClosure is the function type WindowClosure, whose return type is IO LRESULT. Since the return type of postQuitMessage is IO (), which doesn't match, we use return 0 to provide a return value that fits the type.
Also, for window messages other than wM_DESTROY, we need to call defWindowProc for the default behavior.
windowClosure :: WindowClosure
windowClosure hWnd msg w l
| msg == wM_DESTROY = do
postQuitMessage 0
return 0
| otherwise = do
defWindowProc (Just hWnd) msg w l
With this, we can now display a simple window. Although some parts could be refactored to better utilize monads, we have achieved the goal of displaying a window for now.
Complete program
The complete program is as follows.
import Control.Monad (when)
import Graphics.Win32 (iDC_ARROW, loadCursor)
import Graphics.Win32.Message (wM_DESTROY)
import Graphics.Win32.Window
( WindowClosure,
allocaMessage,
createWindow,
defWindowProc,
dispatchMessage,
getMessage,
mkClassName,
registerClass,
sW_SHOWNORMAL,
showWindow,
translateMessage,
updateWindow,
wS_OVERLAPPEDWINDOW,
)
import Graphics.Win32.Window.PostMessage (postQuitMessage)
import System.Win32.DLL (getModuleHandle)
windowClosure :: WindowClosure
windowClosure hWnd msg w l
| msg == wM_DESTROY = do
postQuitMessage 0
return 0
| otherwise = do
defWindowProc (Just hWnd) msg w l
messageLoop :: IO ()
messageLoop = allocaMessage $ \msg -> do
continue <- getMessage msg Nothing
when continue $ do
translateMessage msg
dispatchMessage msg
messageLoop
main :: IO ()
main = do
hModule <- getModuleHandle Nothing
hCursor <- loadCursor Nothing iDC_ARROW
let className = mkClassName "SimpleWindow"
let wndClass =
(0, hModule, Nothing, Just hCursor, Nothing, Nothing, className)
registerClass wndClass
hWnd <-
createWindow
className
"Window Title"
wS_OVERLAPPEDWINDOW
Nothing
Nothing
Nothing
Nothing
Nothing
Nothing
hModule
windowClosure
showWindow hWnd sW_SHOWNORMAL
updateWindow hWnd
messageLoop
return ()
Discussion