Dark theme: what changed
Revision 0 to 1. Removed lines have a minus sign; added lines have a plus sign.
Title: Dark theme
| Old | New | Line | |
|---|---|---|---|
| 1 | + | How to detect whether the operating system is using a dark theme? | |
| 2 | + | ||
| 3 | + | ## Universal method | |
| 4 | + | ||
| 5 | + | The following method is based on the GUI colors. It works on all platforms but those colors are not updated when the theme is changed while the application is running. Thus it cannot allow to detect if the dark mode is toggled. | |
| 6 | + | ||
| 7 | + | ### Example 1 | |
| 8 | + | ||
| 9 | + | ``` pascal | |
| 10 | + | // by "Alextp" from Lazarus forum | |
| 11 | + | function IsDarkTheme: boolean; | |
| 12 | + | const | |
| 13 | + | cMax = $A0; | |
| 14 | + | var | |
| 15 | + | N: TColor; | |
| 16 | + | begin | |
| 17 | + | N:= ColorToRGB(clWindow); | |
| 18 | + | Result:= (Red(N)<cMax) and (Green(N)<cMax) and (Blue(N)<cMax); | |
| 19 | + | end; | |
| 20 | + | ``` | |
| 21 | + | ||
| 22 | + | ### Example 2 | |
| 23 | + | ||
| 24 | + | ``` pascal | |
| 25 | + | // by "Hansaplast" & "Alextp" from Lazarus forum | |
| 26 | + | function IsDarkTheme: boolean; | |
| 27 | + | function _Level(C: TColor): double; | |
| 28 | + | begin | |
| 29 | + | Result:= Red(C)*0.3 + Green(C)*0.59 + Blue(C)*0.11; | |
| 30 | + | end; | |
| 31 | + | begin | |
| 32 | + | Result:= _Level(ColorToRGB(clWindow)) < _Level(ColorToRGB(clWindowText)); | |
| 33 | + | end; | |
| 34 | + | ``` | |
| 35 | + | ||
| 36 | + | ## Windows method | |
| 37 | + | ||
| 38 | + | ### Example 1 | |
| 39 | + | ||
| 40 | + | ``` pascal | |
| 41 | + | // by "jwdietrich" from Lazarus forum | |
| 42 | + | uses | |
| 43 | + | Windows, Win32Proc, Registry; | |
| 44 | + | ||
| 45 | + | // IsDarkTheme: Detects if the Dark Theme (true) has been enabled or not (false) | |
| 46 | + | function IsDarkTheme: boolean; | |
| 47 | + | const | |
| 48 | + | KEYPATH = '\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize'; | |
| 49 | + | KEYNAME = 'AppsUseLightTheme'; | |
| 50 | + | var | |
| 51 | + | LightKey: boolean; | |
| 52 | + | Registry: TRegistry; | |
| 53 | + | begin | |
| 54 | + | Result := false; | |
| 55 | + | Registry := TRegistry.Create; | |
| 56 | + | try | |
| 57 | + | Registry.RootKey := HKEY_CURRENT_USER; | |
| 58 | + | if Registry.OpenKeyReadOnly(KEYPATH) then | |
| 59 | + | begin | |
| 60 | + | if Registry.ValueExists(KEYNAME) then | |
| 61 | + | LightKey := Registry.ReadBool(KEYNAME) | |
| 62 | + | else | |
| 63 | + | LightKey := true; | |
| 64 | + | end | |
| 65 | + | else | |
| 66 | + | LightKey := true; | |
| 67 | + | Result := not LightKey | |
| 68 | + | finally | |
| 69 | + | Registry.Free; | |
| 70 | + | end; | |
| 71 | + | end; | |
| 72 | + | ``` | |
| 73 | + | ||
| 74 | + | ## macOS method | |
| 75 | + | ||
| 76 | + | ### Example 1 | |
| 77 | + | ||
| 78 | + | ``` pascal | |
| 79 | + | // by "trev" from Lazarus forum | |
| 80 | + | ||
| 81 | + | { returns true, if this app runs on macOS 10.14.0 Mojave or newer } | |
| 82 | + | function MojaveOrNewer: boolean; | |
| 83 | + | var | |
| 84 | + | minOsVer: NSOperatingSystemVersion; | |
| 85 | + | begin | |
| 86 | + | //Setup minimum version (Mojave) | |
| 87 | + | minOsVer.majorVersion:= 10; | |
| 88 | + | minOsVer.minorVersion:= 14; | |
| 89 | + | minOsVer.patchVersion:= 0; | |
| 90 | + | ||
| 91 | + | // Check minimum version | |
| 92 | + | if(NSProcessInfo.ProcessInfo.isOperatingSystemAtLeastVersion(minOSVer)) then | |
| 93 | + | Result := True | |
| 94 | + | else | |
| 95 | + | Result := False; | |
| 96 | + | end; | |
| 97 | + | ||
| 98 | + | { The following two functions were suggested by Hansaplast at https://forum.lazarus.freepascal.org/index.php/topic,43111.msg304366.html } | |
| 99 | + | ||
| 100 | + | // Retrieve key's string value from user preferences. Result is encoded using NSStrToStr's default encoding. | |
| 101 | + | function GetPrefString(const KeyName : string) : string; | |
| 102 | + | begin | |
| 103 | + | Result := NSStringToString(NSUserDefaults.standardUserDefaults.stringForKey(NSStr(@KeyName[1]))); | |
| 104 | + | end; | |
| 105 | + | ||
| 106 | + | // IsDarkTheme: Detects if the Dark Theme (true) has been enabled or not (false) | |
| 107 | + | function IsDarkTheme: boolean; | |
| 108 | + | begin | |
| 109 | + | Result := false; | |
| 110 | + | if MojaveOrNewer then | |
| 111 | + | Result := pos('DARK',UpperCase(GetPrefString('AppleInterfaceStyle'))) > 0; | |
| 112 | + | end; | |
| 113 | + | ``` | |
| 114 | + | ||
| 115 | + | #### Demo | |
| 116 | + | ||
| 117 | + | ``` pascal | |
| 118 | + | unit Unit1; | |
| 119 | + | ||
| 120 | + | {$mode objfpc}{$H+} | |
| 121 | + | {$modeswitch objectivec1} | |
| 122 | + | ||
| 123 | + | interface | |
| 124 | + | ||
| 125 | + | uses | |
| 126 | + | Classes, Forms, Dialogs, StdCtrls, SysUtils, | |
| 127 | + | CocoaAll, CocoaUtils, MacOSAll; | |
| 128 | + | ||
| 129 | + | type | |
| 130 | + | ||
| 131 | + | { TForm1 } | |
| 132 | + | ||
| 133 | + | TForm1 = class(TForm) | |
| 134 | + | Button1: TButton; | |
| 135 | + | procedure Button1Click(Sender: TObject); | |
| 136 | + | private | |
| 137 | + | ||
| 138 | + | public | |
| 139 | + | ||
| 140 | + | end; | |
| 141 | + | ||
| 142 | + | var | |
| 143 | + | Form1: TForm1; | |
| 144 | + | ||
| 145 | + | implementation | |
| 146 | + | ||
| 147 | + | {$R *.lfm} | |
| 148 | + | ||
| 149 | + | { TForm1 } | |
| 150 | + | ||
| 151 | + | { returns true, if this app runs on macOS 10.14 Mojave or newer } | |
| 152 | + | function MojaveOrNewer: boolean; | |
| 153 | + | var | |
| 154 | + | minOsVer: NSOperatingSystemVersion; | |
| 155 | + | begin | |
| 156 | + | // Setup minimum version (Mojave) | |
| 157 | + | minOsVer.majorVersion:= 10; | |
| 158 | + | minOsVer.minorVersion:= 14; | |
| 159 | + | minOsVer.patchVersion:= 0; | |
| 160 | + | ||
| 161 | + | // Check minimum version | |
| 162 | + | if(NSProcessInfo.ProcessInfo.isOperatingSystemAtLeastVersion(minOSVer)) then | |
| 163 | + | Result := True | |
| 164 | + | else | |
| 165 | + | Result := False; | |
| 166 | + | end; | |
| 167 | + | ||
| 168 | + | function GetPrefString(const KeyName : string) : string; | |
| 169 | + | begin | |
| 170 | + | Result := NSStringToString(NSUserDefaults.standardUserDefaults.stringForKey(NSStr(@KeyName[1]))); | |
| 171 | + | end; | |
| 172 | + | ||
| 173 | + | function IsDarkTheme: boolean; | |
| 174 | + | begin | |
| 175 | + | Result := false; | |
| 176 | + | if MojaveOrNewer then | |
| 177 | + | Result := pos('DARK',UpperCase(GetPrefString('AppleInterfaceStyle'))) > 0; | |
| 178 | + | end; | |
| 179 | + | ||
| 180 | + | procedure TForm1.Button1Click(Sender: TObject); | |
| 181 | + | begin | |
| 182 | + | if(IsDarkTheme) then | |
| 183 | + | ShowMessage('Dark mode') | |
| 184 | + | else | |
| 185 | + | ShowMessage('Light mode'); | |
| 186 | + | end; | |
| 187 | + | ||
| 188 | + | end. | |
| 189 | + | ``` | |
| 190 | + | ||
| 191 | + | ### Example 2 | |
| 192 | + | ||
| 193 | + | Catalina added an "Auto" option where the computer switches between Light and Dark modes depending on time of day. The 'AppleInterfaceStyle' method apparently doesn't work if auto is enabled. | |
| 194 | + | ||
| 195 | + | The solution is to get the NSApp.effectiveAppearance string which will be one of a number of values including 'NSAppearanceNameAqua' (standard light mode) and 'NSAppearanceNameDarkAqua' (standard dark mode). Google these to see the whole list which includes other light and dark modes with added contrast. The effectiveAppearance is correct in all modes including when auto mode kicks in. | |
| 196 | + | ||
| 197 | + | Tested on Mojave, Catalina, Big Sur. | |
| 198 | + | ||
| 199 | + | ``` pascal | |
| 200 | + | // by "Clover" from Lazarus forum | |
| 201 | + | function IsMacDarkMode: Boolean; | |
| 202 | + | var | |
| 203 | + | sMode: string; | |
| 204 | + | begin | |
| 205 | + | //sMode := CFStringToStr( CFStringRef( NSUserDefaults.StandardUserDefaults.stringForKey( NSSTR('AppleInterfaceStyle') ))); // Doesn't work in auto mode | |
| 206 | + | sMode := CFStringToStr( CFStringRef( NSApp.effectiveAppearance.name )); | |
| 207 | + | Result := Pos('Dark', sMode) > 0; | |
| 208 | + | end; | |
| 209 | + | ``` | |
| 210 | + | ||
| 211 | + | #### Demo | |
| 212 | + | ||
| 213 | + | ``` pascal | |
| 214 | + | unit Unit1; | |
| 215 | + | ||
| 216 | + | {$mode objfpc}{$H+} | |
| 217 | + | {$modeswitch objectivec1} | |
| 218 | + | ||
| 219 | + | interface | |
| 220 | + | ||
| 221 | + | uses | |
| 222 | + | Classes, Forms, Dialogs, StdCtrls, | |
| 223 | + | CocoaAll, CocoaUtils, MacOSAll; | |
| 224 | + | ||
| 225 | + | type | |
| 226 | + | ||
| 227 | + | { TForm1 } | |
| 228 | + | ||
| 229 | + | TForm1 = class(TForm) | |
| 230 | + | Button1: TButton; | |
| 231 | + | procedure Button1Click(Sender: TObject); | |
| 232 | + | private | |
| 233 | + | ||
| 234 | + | public | |
| 235 | + | ||
| 236 | + | end; | |
| 237 | + | ||
| 238 | + | var | |
| 239 | + | Form1: TForm1; | |
| 240 | + | ||
| 241 | + | implementation | |
| 242 | + | ||
| 243 | + | {$R *.lfm} | |
| 244 | + | ||
| 245 | + | { TForm1 } | |
| 246 | + | ||
| 247 | + | function IsMacDarkMode: Boolean; | |
| 248 | + | var | |
| 249 | + | sMode: string; | |
| 250 | + | begin | |
| 251 | + | sMode := CFStringToStr( CFStringRef( NSApp.effectiveAppearance.name )); | |
| 252 | + | Result := Pos('Dark', sMode) > 0; | |
| 253 | + | end; | |
| 254 | + | ||
| 255 | + | procedure TForm1.Button1Click(Sender: TObject); | |
| 256 | + | begin | |
| 257 | + | if(IsMacDarkMode) then | |
| 258 | + | ShowMessage('Dark mode') | |
| 259 | + | else | |
| 260 | + | ShowMessage('Light mode'); | |
| 261 | + | end; | |
| 262 | + | ||
| 263 | + | end. | |
| 264 | + | ``` | |
| 265 | + | ||
| 266 | + | ### Opting out of dark mode | |
| 267 | + | ||
| 268 | + | Mac systems automatically opt in any application linked against the macOS 10.14 or later SDK to both light and dark appearances. You can opt out of dark mode by including the NSRequiresAquaSystemAppearance key (with a value of YES) in your application’s [[macOS property list files|Info.plist]] property list file. Setting this key to YES causes the system to ignore the user's preference and always apply a light appearance to your application. | |
| 269 | + | ||
| 270 | + | ### Opting in to dark mode | |
| 271 | + | ||
| 272 | + | If you build your application against an earlier SDK but still want to support Dark Mode, include the NSRequiresAquaSystemAppearance key (with a value of NO) in your application's [[macOS property list files|Info.plist]] property list file. Do so only if your application's appearance looks correct when running in macOS 10.14 and later with Dark Mode enabled. | |
| 273 | + | ||
| 274 | + | ## See Also | |
| 275 | + | ||
| 276 | + | - [[macOS extensions]] | |
| 277 | + | ||
| 278 | + | ### External links | |
| 279 | + | ||
| 280 | + | - [Apple: Mojave Dark Mode](https://developer.apple.com/design/human-interface-guidelines/macos/visual-design/dark-mode/). | |
| 281 | + | ||
| 282 | + | --- | |
| 283 | + | *From the Free Pascal wiki, page "Dark theme", as it stood on 2021-01-31 after a change by Jwdietrich, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).* | |
| 284 | + | Category: Code Snippets | |
| 285 | + | Category: FPC | |
| 286 | + | Category: Platform-sensitive development | |
| 287 | + | Category: Windows | |
| 288 | + | Category: macOS |
