diff --git a/Setup.hs b/Setup.hs
--- a/Setup.hs
+++ b/Setup.hs
@@ -1,6 +1,6 @@
 -- This file is part of Qtah.
 --
--- Copyright 2015-2021 The Qtah Authors.
+-- Copyright 2015-2023 The Qtah Authors.
 --
 -- This program is free software: you can redistribute it and/or modify
 -- it under the terms of the GNU Lesser General Public License as published by
diff --git a/qtah-examples.cabal b/qtah-examples.cabal
--- a/qtah-examples.cabal
+++ b/qtah-examples.cabal
@@ -1,12 +1,12 @@
 name: qtah-examples
-version: 0.8.0
+version: 0.9.0
 synopsis: Example programs for Qtah Qt bindings
-homepage: http://khumba.net/projects/qtah
+homepage: https://khumba.net/projects/qtah
 license: LGPL-3
 license-files: LICENSE.GPL, LICENSE.LGPL
 author: Bryan Gardiner <bog@khumba.net>
 maintainer: Bryan Gardiner <bog@khumba.net>
-copyright: Copyright 2015-2021 The Qtah Authors.
+copyright: Copyright 2015-2025 The Qtah Authors.
 category: Graphics
 build-type: Simple
 cabal-version: 2.0
@@ -22,12 +22,12 @@
   other-extensions:
       ScopedTypeVariables
   build-depends:
-      base >=4 && <5
+      base >=4.10 && <5
     , binary >=0.7 && <0.9
-    , bytestring >=0.10 && <0.11
+    , bytestring >=0.10 && <0.12
     , containers <0.7
     , filepath >=1.0 && <1.5
-    , hoppy-runtime >=0.8 && <0.9
-    , qtah-qt5 >=0.8 && <0.9
+    , hoppy-runtime >=0.9 && <0.10
+    , qtah-qt6 >=0.9 && <0.10
   ghc-options: -rtsopts -W -fwarn-incomplete-patterns -fwarn-unused-do-bind -dynamic
   default-language: Haskell2010
diff --git a/src/Graphics/UI/Qtah/Example/Notepad.hs b/src/Graphics/UI/Qtah/Example/Notepad.hs
--- a/src/Graphics/UI/Qtah/Example/Notepad.hs
+++ b/src/Graphics/UI/Qtah/Example/Notepad.hs
@@ -1,6 +1,6 @@
 -- This file is part of Qtah.
 --
--- Copyright 2015-2021 The Qtah Authors.
+-- Copyright 2015-2024 The Qtah Authors.
 --
 -- This program is free software: you can redistribute it and/or modify
 -- it under the terms of the GNU Lesser General Public License as published by
@@ -29,6 +29,10 @@
 import Graphics.UI.Qtah.Gui.QCloseEvent (QCloseEvent)
 import Graphics.UI.Qtah.Signal (connect_)
 import qualified Graphics.UI.Qtah.Core.QEvent as QEvent
+import qualified Graphics.UI.Qtah.Gui.QTextDocument as QTextDocument
+import Graphics.UI.Qtah.Gui.QTextDocument (
+  modificationChangedSignal,
+  )
 import qualified Graphics.UI.Qtah.Widgets.QAction as QAction
 import Graphics.UI.Qtah.Widgets.QAction (triggeredSignal)
 import qualified Graphics.UI.Qtah.Widgets.QFileDialog as QFileDialog
@@ -42,7 +46,6 @@
   QTextEdit,
   copyAvailableSignal,
   redoAvailableSignal,
-  textChangedSignal,
   undoAvailableSignal,
   )
 import qualified Graphics.UI.Qtah.Widgets.QWidget as QWidget
@@ -52,7 +55,6 @@
   { myWindow :: QMainWindow
   , myText :: QTextEdit
   , myFilePathRef :: IORef (Maybe FilePath)
-  , myDirtyRef :: IORef Bool
   }
 
 run :: IO ()
@@ -89,23 +91,36 @@
   forM_ [menuEditUndo, menuEditRedo, menuEditCut, menuEditCopy] $ \action ->
     QAction.setEnabled action False
 
+  menuHelp <- QMenuBar.addNewMenu menu "&Help"
+  menuHelpAboutQt <- QMenu.addNewAction menuHelp "About &Qt"
+
   text <- QTextEdit.new
   QMainWindow.setCentralWidget window text
   QTextEdit.setUndoRedoEnabled text True
+  doc <- QTextEdit.document text
 
   filePathRef <- newIORef Nothing
-  dirtyRef <- newIORef False
 
   let me = Notepad
            { myWindow = window
            , myText = text
            , myFilePathRef = filePathRef
-           , myDirtyRef = dirtyRef
            }
 
   _ <- onEvent window $ \(event :: QCloseEvent) -> do
     continue <- confirmSaveIfDirty me "Quit"
-    unless continue $ QEvent.ignore event
+
+    -- Qt delivers QCloseEvent to the window twice under certain circumstances,
+    -- for example on Linux when closing the window with the keyboard (reliably
+    -- with Alt+F4, and intermittently with Alt+Space C, on both Openbox and
+    -- KWin).  To avoid getting prompted to save a second time after clicking No
+    -- when the document is dirty, we manually clear the dirty state if we know
+    -- we're going to close.
+    if continue
+      then do QTextDocument.setModified doc False
+              QEvent.accept event
+      else QEvent.ignore event
+
     return $ not continue
 
   connect_ menuFileNew triggeredSignal $ \_ -> fileNew me
@@ -124,13 +139,16 @@
   connect_ menuEditPaste triggeredSignal $ \_ -> QTextEdit.paste text
   connect_ menuEditSelectAll triggeredSignal $ \_ -> QTextEdit.selectAll text
 
-  connect_ text textChangedSignal $ setDirty me True
+  connect_ menuHelpAboutQt triggeredSignal $ \_ -> QMessageBox.aboutQt window "About Qt"
+
   connect_ text undoAvailableSignal $ \b -> QAction.setEnabled menuEditUndo b
   connect_ text redoAvailableSignal $ \b -> QAction.setEnabled menuEditRedo b
   connect_ text copyAvailableSignal $ \b -> do
     QAction.setEnabled menuEditCut b
     QAction.setEnabled menuEditCopy b
 
+  connect_ doc modificationChangedSignal $ \_ -> updateTitle me
+
   updateTitle me
   return window
 
@@ -183,8 +201,7 @@
 -- editor was not dirty, or if the editor was dirty and the save was performed.
 confirmSaveIfDirty :: Notepad -> String -> IO Bool
 confirmSaveIfDirty me title = do
-  let dirtyRef = myDirtyRef me
-  dirty <- readIORef dirtyRef
+  dirty <- QTextDocument.isModified =<< QTextEdit.document (myText me)
   if dirty
     then do response <- QMessageBox.questionWithButtons
                         (myWindow me)
@@ -202,11 +219,8 @@
 
 setDirty :: Notepad -> Bool -> IO ()
 setDirty me dirty = do
-  let ref = myDirtyRef me
-  dirtyOld <- readIORef ref
-  when (dirty /= dirtyOld) $ do
-    writeIORef ref dirty
-    updateTitle me
+  doc <- QTextEdit.document $ myText me
+  QTextDocument.setModified doc dirty
 
 setFilePath :: Notepad -> Maybe FilePath -> IO ()
 setFilePath me path = do
@@ -215,7 +229,7 @@
 
 updateTitle :: Notepad -> IO ()
 updateTitle me = do
-  dirty <- readIORef $ myDirtyRef me
+  dirty <- QTextDocument.isModified =<< QTextEdit.document (myText me)
   file <- fmap (maybe "(Untitled)" takeFileName) $ readIORef $ myFilePathRef me
   QWidget.setWindowTitle (myWindow me) $
     (if dirty then ('*':) else id) $
diff --git a/src/Main.hs b/src/Main.hs
--- a/src/Main.hs
+++ b/src/Main.hs
@@ -1,6 +1,6 @@
 -- This file is part of Qtah.
 --
--- Copyright 2015-2021 The Qtah Authors.
+-- Copyright 2015-2024 The Qtah Authors.
 --
 -- This program is free software: you can redistribute it and/or modify
 -- it under the terms of the GNU Lesser General Public License as published by
@@ -29,9 +29,7 @@
 import qualified Graphics.UI.Qtah.Core.QModelIndex as QModelIndex
 import qualified Graphics.UI.Qtah.Core.QStringListModel as QStringListModel
 import qualified Graphics.UI.Qtah.Core.QVariant as QVariant
-import Graphics.UI.Qtah.Event (onEvent)
 import qualified Graphics.UI.Qtah.Example.Notepad as Notepad
-import qualified Graphics.UI.Qtah.Gui.QCloseEvent as QCloseEvent
 import qualified Graphics.UI.Qtah.Gui.QFont as QFont
 import qualified Graphics.UI.Qtah.Widgets.QAbstractButton as QAbstractButton
 import qualified Graphics.UI.Qtah.Widgets.QAbstractItemView as QAbstractItemView
@@ -129,10 +127,6 @@
               , uiCurrentExampleRef = currentExampleRef
               , uiDescriptionLabel = descriptionLabel
               }
-
-  _ <- onEvent window $ \(_ :: QCloseEvent.QCloseEvent) -> do
-    QCoreApplication.quit
-    return False
 
   selectionModel <- QAbstractItemView.selectionModel listView
   connect_ selectionModel QItemSelectionModel.currentChangedSignal $ \index _ ->
