src/GUI/DarkMode.ps1
|
# 读取系统「应用」深浅色;读不到时按浅色处理。 function Get-SystemDarkMode { $light = Get-ItemPropertyValue -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize" -Name "AppsUseLightTheme" -ErrorAction SilentlyContinue return $light -eq 0 } $Script:SystemDarkMode = Get-SystemDarkMode if ($UIMode -eq 'Auto' -or -not $UIMode) { $Script:DarkMode = $Script:SystemDarkMode } else { $Script:DarkMode = $UIMode -eq 'Dark' } # 用户手动点过深色按钮后置位;仅当系统设置本身发生变化时才让自动跟随重新接管。 $Script:DarkModeOverride = $false # 亮/暗共用一套语义调色板,深色走现代扁平风:窗体底色最暗、分组卡片略亮、输入控件内凹。 # 颜色一律以 #RRGGBB 字符串存放,应用时再转 Color,避免脚本里散落硬编码。 function Get-GUIThemePalette { param([bool]$Dark) if ($Dark) { return @{ WindowBack = '#1e1e1e' SurfaceBack = '#252526' SurfaceAlt = '#2d2d30' InputBack = '#1b1b1c' Border = '#3f3f46' Text = '#e6e6e6' MutedText = '#9d9d9d' Accent = '#0a84ff' AccentText = '#ffffff' ButtonBack = '#333337' ButtonHover = '#3f3f46' ButtonDown = '#474750' ButtonBorder = '#4a4a52' TabHover = '#2d2d30' TabActive = '#1e1e1e' LogBack = '#141415' LogText = '#c8c8c8' GridLine = '#3f3f46' BorderRgb = 0x1e1e1e } } return @{ WindowBack = '#f3f3f3' SurfaceBack = '#ffffff' SurfaceAlt = '#fafafa' InputBack = '#ffffff' Border = '#d6d6d6' Text = '#1f1f1f' MutedText = '#5f5f5f' Accent = '#0067c0' AccentText = '#ffffff' ButtonBack = '#fdfdfd' ButtonHover = '#f0f0f0' ButtonDown = '#e5e5e5' ButtonBorder = '#cccccc' TabHover = '#e8e8e8' TabActive = '#f3f3f3' LogBack = '#ffffff' LogText = '#1f1f1f' GridLine = '#d6d6d6' BorderRgb = 0xf3f3f3 } } function ConvertTo-GUIColor { param([string]$Html) return [System.Drawing.ColorTranslator]::FromHtml($Html) } # 图标源图是纯白透明 PNG,直接当 BackgroundImage 会被裁成 256px 的一角。 # 缩到 16px 并按主题前景色着色后挂到 Button.Image 上,亮/暗下都清晰。 function Set-GUIButtonIcon { param( [System.Windows.Forms.Button]$Button, [string]$SourcePath, [System.Drawing.Color]$Color ) if (-not $Button) { return } if ($Button.Image) { $previous = $Button.Image $Button.Image = $null $previous.Dispose() } if (-not (Test-Path -LiteralPath $SourcePath)) { return } $source = [System.Drawing.Image]::FromFile($SourcePath) try { $size = 16 $bitmap = New-Object System.Drawing.Bitmap($size, $size) $graphics = [System.Drawing.Graphics]::FromImage($bitmap) try { $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic $matrix = New-Object System.Drawing.Imaging.ColorMatrix $matrix.Matrix00 = $Color.R / 255 $matrix.Matrix11 = $Color.G / 255 $matrix.Matrix22 = $Color.B / 255 $matrix.Matrix33 = 1 $matrix.Matrix44 = 1 $attributes = New-Object System.Drawing.Imaging.ImageAttributes $attributes.SetColorMatrix($matrix) $dest = New-Object System.Drawing.Rectangle(0, 0, $size, $size) $graphics.DrawImage($source, $dest, 0, 0, $source.Width, $source.Height, [System.Drawing.GraphicsUnit]::Pixel, $attributes) $attributes.Dispose() } finally { $graphics.Dispose() } $Button.Image = $bitmap } finally { $source.Dispose() } } # 自绘标签页:WinForms 的 TabControl 跟不了深色,手动画成扁平样式(选中项加一条强调色下划线)。 # 只需挂钩一次,之后换肤靠 $Script:GUITheme 重绘。 function Initialize-GUITabTheme { param([System.Windows.Forms.TabControl]$Tabs) if ($Script:GUIThemeTabsHooked) { return } $Script:GUIThemeTabsHooked = $true $Tabs.DrawMode = [System.Windows.Forms.TabDrawMode]::OwnerDrawFixed $Tabs.SizeMode = [System.Windows.Forms.TabSizeMode]::Normal $Tabs.ItemSize = New-Object System.Drawing.Size(0, [int]($Tabs.Font.Height + 16)) $Tabs.add_DrawItem({ param($sender, $e) $palette = $Script:GUITheme if (-not $palette -or $e.Index -lt 0 -or $e.Index -ge $sender.TabPages.Count) { return } $graphics = $e.Graphics $bounds = $e.Bounds $selected = ($e.Index -eq $sender.SelectedIndex) $hot = (($e.State -band [System.Windows.Forms.DrawItemState]::HotLight) -eq [System.Windows.Forms.DrawItemState]::HotLight) $backHtml = if ($selected) { $palette.TabActive } elseif ($hot) { $palette.TabHover } else { $palette.WindowBack } $brush = New-Object System.Drawing.SolidBrush((ConvertTo-GUIColor $backHtml)) try { $graphics.FillRectangle($brush, $bounds) } finally { $brush.Dispose() } if ($selected) { $barBrush = New-Object System.Drawing.SolidBrush((ConvertTo-GUIColor $palette.Accent)) try { $graphics.FillRectangle($barBrush, [int]$bounds.X, [int]($bounds.Bottom - 3), [int]$bounds.Width, 3) } finally { $barBrush.Dispose() } } $textColor = ConvertTo-GUIColor $(if ($selected) { $palette.Text } else { $palette.MutedText }) $textRect = New-Object System.Drawing.Rectangle([int]$bounds.X, [int]$bounds.Y, [int]$bounds.Width, [int]($bounds.Height - 2)) $flags = [System.Windows.Forms.TextFormatFlags]::HorizontalCenter -bor [System.Windows.Forms.TextFormatFlags]::VerticalCenter -bor [System.Windows.Forms.TextFormatFlags]::EndEllipsis [System.Windows.Forms.TextRenderer]::DrawText($graphics, [string]$sender.TabPages[$e.Index].Text, $sender.Font, $textRect, $textColor, $flags) }) } function Set-DarkMode { param($set) if (-not $Script:refs -or -not $Script:refs.MainForm) { return } $mainForm = $Script:refs.MainForm $dark = [bool]$set $palette = Get-GUIThemePalette -Dark $dark $Script:GUITheme = $palette $windowBack = ConvertTo-GUIColor $palette.WindowBack $surfaceBack = ConvertTo-GUIColor $palette.SurfaceBack $surfaceAlt = ConvertTo-GUIColor $palette.SurfaceAlt $inputBack = ConvertTo-GUIColor $palette.InputBack $text = ConvertTo-GUIColor $palette.Text $mutedText = ConvertTo-GUIColor $palette.MutedText $accent = ConvertTo-GUIColor $palette.Accent $accentText = ConvertTo-GUIColor $palette.AccentText $buttonBack = ConvertTo-GUIColor $palette.ButtonBack $buttonHover = ConvertTo-GUIColor $palette.ButtonHover $buttonDown = ConvertTo-GUIColor $palette.ButtonDown $buttonBorder = ConvertTo-GUIColor $palette.ButtonBorder $logBack = ConvertTo-GUIColor $palette.LogBack $logText = ConvertTo-GUIColor $palette.LogText function Set-ControlWindowTheme($control, [bool]$isDark) { try { if ($isDark) { [void][ps12exeGUI.Win32]::SetWindowTheme($control.Handle, 'DarkMode_Explorer', $null) } else { [void][ps12exeGUI.Win32]::SetWindowTheme($control.Handle, $null, $null) } } catch { } } function Set-ControlTheme($control) { $parentBack = if ($control.Parent) { $control.Parent.BackColor } else { $windowBack } if ($control -is [System.Windows.Forms.TextBoxBase]) { $isLog = ($control -eq $Script:refs.LogTextBox) $control.BackColor = if ($isLog) { $logBack } else { $inputBack } $control.ForeColor = if ($isLog) { $logText } else { $text } $control.BorderStyle = 'FixedSingle' Set-ControlWindowTheme $control $dark } elseif ($control -is [System.Windows.Forms.DataGridView]) { $control.EnableHeadersVisualStyles = $false $control.BackgroundColor = $surfaceBack $control.ForeColor = $text $control.GridColor = ConvertTo-GUIColor $palette.GridLine $control.BorderStyle = 'FixedSingle' $control.DefaultCellStyle.BackColor = $inputBack $control.DefaultCellStyle.ForeColor = $text $control.DefaultCellStyle.SelectionBackColor = $accent $control.DefaultCellStyle.SelectionForeColor = $accentText $control.ColumnHeadersDefaultCellStyle.BackColor = $surfaceAlt $control.ColumnHeadersDefaultCellStyle.ForeColor = $mutedText $control.ColumnHeadersDefaultCellStyle.SelectionBackColor = $surfaceAlt $control.ColumnHeadersDefaultCellStyle.SelectionForeColor = $mutedText $control.RowHeadersDefaultCellStyle.BackColor = $surfaceBack $control.RowHeadersDefaultCellStyle.ForeColor = $text Set-ControlWindowTheme $control $dark } elseif ($control -is [System.Windows.Forms.ComboBox] -or $control -is [System.Windows.Forms.ListBox]) { $control.BackColor = $inputBack $control.ForeColor = $text $control.FlatStyle = 'Flat' Set-ControlWindowTheme $control $dark } elseif ($control -is [System.Windows.Forms.Button]) { $control.FlatStyle = 'Flat' $control.UseVisualStyleBackColor = $false $flat = $control.FlatAppearance if ($control -eq $Script:refs.CompileButton -or $control -eq $Script:refs.CancelButton) { $control.BackColor = $accent $control.ForeColor = $accentText $flat.BorderSize = 0 $flat.MouseOverBackColor = [System.Windows.Forms.ControlPaint]::Light($accent, 0.15) $flat.MouseDownBackColor = [System.Windows.Forms.ControlPaint]::Dark($accent, 0.12) } else { $control.BackColor = $buttonBack $control.ForeColor = $text $flat.BorderSize = 1 $flat.BorderColor = $buttonBorder $flat.MouseOverBackColor = $buttonHover $flat.MouseDownBackColor = $buttonDown } } elseif ($control -is [System.Windows.Forms.CheckBox] -or $control -is [System.Windows.Forms.RadioButton]) { $control.ForeColor = $text $control.FlatStyle = 'Flat' $control.UseVisualStyleBackColor = $false $control.BackColor = [System.Drawing.Color]::Transparent } elseif ($control -is [System.Windows.Forms.GroupBox]) { $control.ForeColor = $mutedText $control.BackColor = $surfaceBack if ($control -is [ps12exeGUI.FlatGroupBox]) { $control.BorderColor = ConvertTo-GUIColor $palette.Border $control.TitleColor = $mutedText } } elseif ($control -is [System.Windows.Forms.Label]) { $control.ForeColor = $text $control.BackColor = [System.Drawing.Color]::Transparent } elseif ($control -is [System.Windows.Forms.TabControl]) { $control.BackColor = $windowBack Initialize-GUITabTheme $control } elseif ($control -is [System.Windows.Forms.TabPage]) { $control.BackColor = $windowBack $control.ForeColor = $text } elseif ($control -is [System.Windows.Forms.Panel]) { $control.BackColor = $parentBack } if ($control.HasChildren) { foreach ($child in $control.Controls) { Set-ControlTheme $child } } } $mainForm.BackColor = $windowBack $mainForm.ForeColor = $text Set-ControlTheme $mainForm # 提示框跟主题走,否则深色下是一块扎眼的浅色。 if ($Script:GUIToolTip) { $Script:GUIToolTip.BackColor = $surfaceBack $Script:GUIToolTip.ForeColor = $text } if ($Script:refs.TabsControl) { $Script:refs.TabsControl.Invalidate() } # 图标按钮的图标随主题前景色重新着色。 Set-GUIButtonIcon $Script:refs.DarkModeSetButton "$PSScriptRoot\..\..\img\darklight.png" $text Set-GUIButtonIcon $Script:refs.BGMSetButton "$PSScriptRoot\..\..\img\music.png" $text # DWMWA_USE_IMMERSIVE_DARK_MODE [ps12exeGUI.Dwm]::SetWindowAttribute($mainForm.Handle, 20, [int]$dark) # DWMWA_BORDER_COLOR(颜色通道需要从 RGB 转成 BGR) $borderRgb = [int]$palette.BorderRgb $color = (($borderRgb -band 0xff) -shl 16) + ($borderRgb -band 0xff00) + (($borderRgb -shr 16) -band 0xff) [ps12exeGUI.Dwm]::SetWindowAttribute($mainForm.Handle, 34, $color) } # 跟随系统深浅色:只有系统设置本身发生变化才算「系统切换」,用户手动覆盖在此期间保持不变,系统再变则自动跟随接管。 # -SystemDark 仅供测试注入,缺省读注册表。 function Sync-GUIDarkMode { param([bool]$SystemDark) if (-not $PSBoundParameters.ContainsKey('SystemDark')) { $SystemDark = Get-SystemDarkMode } if ($SystemDark -ne $Script:SystemDarkMode) { $Script:SystemDarkMode = $SystemDark $Script:DarkModeOverride = $false if ($SystemDark -ne $Script:DarkMode) { $Script:DarkMode = $SystemDark Set-DarkMode $SystemDark } } } # UIMode 为 Auto(或未指定)时,定时跟随系统深浅色切换。 if ($UIMode -eq 'Auto' -or -not $UIMode) { if (-not $Script:DarkModeTimer) { $Script:DarkModeTimer = New-Object System.Windows.Forms.Timer $Script:DarkModeTimer.Interval = 2000 $Script:DarkModeTimer.add_Tick({ Sync-GUIDarkMode }) $Script:DarkModeTimer.Start() } } |