This commit is contained in:
Yu Leng
2025-10-15 18:07:10 +08:00
parent 5747e5e537
commit f8bbfbb6a7
80 changed files with 11423 additions and 6 deletions

144
.editorconfig Normal file
View File

@@ -0,0 +1,144 @@
# EditorConfig for PowerToys - Global Settings
root = true
[*.cs]
# 禁用过于严格的StyleCop规则
# 文件结尾换行符
dotnet_diagnostic.SA1518.severity = none
# 空行相关
dotnet_diagnostic.SA1505.severity = none
dotnet_diagnostic.SA1507.severity = none
dotnet_diagnostic.SA1508.severity = none
dotnet_diagnostic.SA1513.severity = none
dotnet_diagnostic.SA1515.severity = none
dotnet_diagnostic.SA1516.severity = none
# 命名规则 (对于Windows API结构)
dotnet_diagnostic.SA1307.severity = none
dotnet_diagnostic.SA1313.severity = none
# Using指令排序
dotnet_diagnostic.SA1211.severity = none
# 多行初始化器尾随逗号
dotnet_diagnostic.SA1413.severity = none
# 参数格式
dotnet_diagnostic.SA1116.severity = none
dotnet_diagnostic.SA1117.severity = none
dotnet_diagnostic.SA1111.severity = none
dotnet_diagnostic.SA1128.severity = none
# 大括号
dotnet_diagnostic.SA1503.severity = none
# 代码格式
dotnet_diagnostic.SA1025.severity = none
dotnet_diagnostic.SA1028.severity = none
dotnet_diagnostic.SA1108.severity = none
dotnet_diagnostic.SA1122.severity = none
dotnet_diagnostic.SA1129.severity = none
dotnet_diagnostic.SA1137.severity = none
dotnet_diagnostic.SA1407.severity = none
# 文件名匹配
dotnet_diagnostic.SA1402.severity = none
dotnet_diagnostic.SA1649.severity = none
# P/Invoke相关警告
dotnet_diagnostic.CA1401.severity = none
dotnet_diagnostic.CA2101.severity = none
# 其他代码分析警告
dotnet_diagnostic.CA1001.severity = none
dotnet_diagnostic.CA1305.severity = none
dotnet_diagnostic.CA1805.severity = none
dotnet_diagnostic.CA1806.severity = none
dotnet_diagnostic.CA1816.severity = none
dotnet_diagnostic.CA1825.severity = none
# 代码样式设置
indent_style = space
indent_size = 4
end_of_line = crlf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
csharp_using_directive_placement = outside_namespace:silent
csharp_prefer_simple_using_statement = true:suggestion
csharp_prefer_braces = true:silent
csharp_style_namespace_declarations = block_scoped:silent
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_top_level_statements = true:silent
csharp_style_prefer_primary_constructors = true:suggestion
csharp_prefer_system_threading_lock = true:suggestion
csharp_style_prefer_simple_property_accessors = true:suggestion
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = when_on_single_line:suggestion
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent
[*.{cs,vb}]
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
indent_size = 4
end_of_line = crlf
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_object_initializer = false:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion

97
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,97 @@
{
"cmake.ignoreCMakeListsMissing": true,
"files.associations": {
"algorithm": "cpp",
"array": "cpp",
"atomic": "cpp",
"bit": "cpp",
"cctype": "cpp",
"charconv": "cpp",
"chrono": "cpp",
"cinttypes": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"compare": "cpp",
"complex": "cpp",
"concepts": "cpp",
"condition_variable": "cpp",
"coroutine": "cpp",
"cstddef": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"cwctype": "cpp",
"deque": "cpp",
"exception": "cpp",
"execution": "cpp",
"filesystem": "cpp",
"format": "cpp",
"forward_list": "cpp",
"fstream": "cpp",
"functional": "cpp",
"future": "cpp",
"initializer_list": "cpp",
"iomanip": "cpp",
"ios": "cpp",
"iosfwd": "cpp",
"iostream": "cpp",
"istream": "cpp",
"iterator": "cpp",
"limits": "cpp",
"list": "cpp",
"locale": "cpp",
"map": "cpp",
"memory": "cpp",
"memory_resource": "cpp",
"mutex": "cpp",
"new": "cpp",
"numeric": "cpp",
"optional": "cpp",
"ostream": "cpp",
"queue": "cpp",
"random": "cpp",
"ratio": "cpp",
"regex": "cpp",
"set": "cpp",
"shared_mutex": "cpp",
"source_location": "cpp",
"span": "cpp",
"sstream": "cpp",
"stdexcept": "cpp",
"stop_token": "cpp",
"streambuf": "cpp",
"string": "cpp",
"strstream": "cpp",
"system_error": "cpp",
"thread": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"typeindex": "cpp",
"typeinfo": "cpp",
"unordered_map": "cpp",
"unordered_set": "cpp",
"utility": "cpp",
"valarray": "cpp",
"variant": "cpp",
"vector": "cpp",
"xfacet": "cpp",
"xhash": "cpp",
"xiosbase": "cpp",
"xlocale": "cpp",
"xlocbuf": "cpp",
"xlocinfo": "cpp",
"xlocmes": "cpp",
"xlocmon": "cpp",
"xlocnum": "cpp",
"xloctime": "cpp",
"xmemory": "cpp",
"xstddef": "cpp",
"xstring": "cpp",
"xtr1common": "cpp",
"xtree": "cpp",
"xutility": "cpp"
}
}

View File

@@ -23,7 +23,21 @@
<Version>$(Version).0</Version>
<RepositoryUrl>https://github.com/microsoft/PowerToys</RepositoryUrl>
<RepositoryType>GitHub</RepositoryType>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
<RunAnalyzersDuringLiveAnalysis>false</RunAnalyzersDuringLiveAnalysis>
<!-- Disable ALL StyleCop warnings -->
<NoWarn>$(NoWarn);SA0001;SA1000;SA1001;SA1002;SA1003;SA1004;SA1005;SA1006;SA1007;SA1008;SA1009;SA1010;SA1011;SA1012;SA1013;SA1014;SA1015;SA1016;SA1017;SA1018;SA1019;SA1020;SA1021;SA1022;SA1023;SA1024;SA1025;SA1026;SA1027;SA1028</NoWarn>
<NoWarn>$(NoWarn);SA1100;SA1101;SA1102;SA1103;SA1104;SA1105;SA1106;SA1107;SA1108;SA1109;SA1110;SA1111;SA1112;SA1113;SA1114;SA1115;SA1116;SA1117;SA1118;SA1119;SA1120;SA1121;SA1122;SA1123;SA1124;SA1125;SA1126;SA1127;SA1128;SA1129;SA1130;SA1131;SA1132;SA1133;SA1134;SA1135;SA1136;SA1137;SA1138;SA1139</NoWarn>
<NoWarn>$(NoWarn);SA1200;SA1201;SA1202;SA1203;SA1204;SA1205;SA1206;SA1207;SA1208;SA1209;SA1210;SA1211;SA1212;SA1213;SA1214;SA1215;SA1216;SA1217</NoWarn>
<NoWarn>$(NoWarn);SA1300;SA1301;SA1302;SA1303;SA1304;SA1305;SA1306;SA1307;SA1308;SA1309;SA1310;SA1311;SA1312;SA1313;SA1314</NoWarn>
<NoWarn>$(NoWarn);SA1400;SA1401;SA1402;SA1403;SA1404;SA1405;SA1406;SA1407;SA1408;SA1409;SA1410;SA1411;SA1412;SA1413</NoWarn>
<NoWarn>$(NoWarn);SA1500;SA1501;SA1502;SA1503;SA1504;SA1505;SA1506;SA1507;SA1508;SA1509;SA1510;SA1511;SA1512;SA1513;SA1514;SA1515;SA1516;SA1517;SA1518;SA1519;SA1520</NoWarn>
<NoWarn>$(NoWarn);SA1600;SA1601;SA1602;SA1603;SA1604;SA1605;SA1606;SA1607;SA1608;SA1609;SA1610;SA1611;SA1612;SA1613;SA1614;SA1615;SA1616;SA1617;SA1618;SA1619;SA1620;SA1621;SA1622;SA1623;SA1624;SA1625;SA1626;SA1627;SA1628;SA1629;SA1630;SA1631;SA1632;SA1633;SA1634;SA1635;SA1636;SA1637;SA1638;SA1639;SA1640;SA1641;SA1642;SA1643;SA1644;SA1645;SA1646;SA1647;SA1648;SA1649;SA1650;SA1651;SA1652</NoWarn>
<!-- Disable specific Code Analysis warnings -->
<NoWarn>$(NoWarn);CA1001;CA1002;CA1003;CA1004;CA1005;CA1006;CA1007;CA1008;CA1009;CA1010;CA1011;CA1012;CA1013;CA1014;CA1016;CA1017;CA1018;CA1019;CA1020;CA1021;CA1022;CA1023;CA1024;CA1025;CA1026;CA1027;CA1028;CA1030;CA1031;CA1032;CA1033;CA1034;CA1035;CA1036;CA1038;CA1039;CA1040;CA1041;CA1043;CA1044;CA1045;CA1046;CA1047;CA1048;CA1049;CA1050;CA1051;CA1052;CA1053;CA1054;CA1055;CA1056;CA1057;CA1058;CA1059;CA1060;CA1061;CA1062;CA1063;CA1064;CA1065;CA1066;CA1067;CA1068;CA1069</NoWarn>
<NoWarn>$(NoWarn);CA1305;CA1401;CA1806;CA1816;CA1825;CA2101;CA2201</NoWarn>
</PropertyGroup>
<PropertyGroup>
@@ -31,7 +45,7 @@
<ForceImportBeforeCppProps>$(MsbuildThisFileDirectory)\Cpp.Build.props</ForceImportBeforeCppProps>
</PropertyGroup>
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj' AND '$(DisableStyleCop)' != 'true'">
<PackageReference Include="StyleCop.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -45,7 +45,7 @@
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.8" />
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.15.0" />
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3179.45" />
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.8" />
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
@@ -57,8 +57,8 @@
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
-->
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4654" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />

View File

@@ -512,6 +512,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RegistryPreviewExt", "src\m
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RegistryPreview", "RegistryPreview", "{929C1324-22E8-4412-A9A8-80E85F3985A5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PowerDisplay", "PowerDisplay", "{B5E6F789-0123-4567-8901-23456789ABCD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FilePreviewCommon", "src\common\FilePreviewCommon\FilePreviewCommon.csproj", "{9EBAA524-0EDA-470B-95D4-39383285CBB2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.PowerToys", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.PowerToys\Microsoft.PowerToys.Run.Plugin.PowerToys.csproj", "{500DED3E-CFB5-4ED5-ACC6-02B3D6DC336D}"
@@ -558,6 +560,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts", "src\modules\Hosts\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RegistryPreview", "src\modules\registrypreview\RegistryPreview\RegistryPreview.csproj", "{8E23E173-7127-4A5F-9F93-3049F2B68047}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerDisplay", "src\modules\powerdisplay\PowerDisplay\PowerDisplay.csproj", "{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerDisplayExt", "src\modules\powerdisplay\PowerDisplayExt\PowerDisplayExt.vcxproj", "{D1234567-8901-2345-6789-ABCDEF012345}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvironmentVariables", "src\modules\EnvironmentVariables\EnvironmentVariables\EnvironmentVariables.csproj", "{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FancyZonesEditorCommon", "src\modules\fancyzones\FancyZonesEditorCommon\FancyZonesEditorCommon.csproj", "{C0974915-8A1D-4BF0-977B-9587D3807AB7}"
@@ -2213,6 +2219,22 @@ Global
{8E23E173-7127-4A5F-9F93-3049F2B68047}.Release|ARM64.Build.0 = Release|ARM64
{8E23E173-7127-4A5F-9F93-3049F2B68047}.Release|x64.ActiveCfg = Release|x64
{8E23E173-7127-4A5F-9F93-3049F2B68047}.Release|x64.Build.0 = Release|x64
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|ARM64.ActiveCfg = Debug|ARM64
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|ARM64.Build.0 = Debug|ARM64
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|x64.ActiveCfg = Debug|x64
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|x64.Build.0 = Debug|x64
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|ARM64.ActiveCfg = Release|ARM64
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|ARM64.Build.0 = Release|ARM64
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|x64.ActiveCfg = Release|x64
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|x64.Build.0 = Release|x64
{D1234567-8901-2345-6789-ABCDEF012345}.Debug|ARM64.ActiveCfg = Debug|ARM64
{D1234567-8901-2345-6789-ABCDEF012345}.Debug|ARM64.Build.0 = Debug|ARM64
{D1234567-8901-2345-6789-ABCDEF012345}.Debug|x64.ActiveCfg = Debug|x64
{D1234567-8901-2345-6789-ABCDEF012345}.Debug|x64.Build.0 = Debug|x64
{D1234567-8901-2345-6789-ABCDEF012345}.Release|ARM64.ActiveCfg = Release|ARM64
{D1234567-8901-2345-6789-ABCDEF012345}.Release|ARM64.Build.0 = Release|ARM64
{D1234567-8901-2345-6789-ABCDEF012345}.Release|x64.ActiveCfg = Release|x64
{D1234567-8901-2345-6789-ABCDEF012345}.Release|x64.Build.0 = Release|x64
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Debug|ARM64.ActiveCfg = Debug|ARM64
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Debug|ARM64.Build.0 = Debug|ARM64
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Debug|x64.ActiveCfg = Debug|x64
@@ -2932,7 +2954,6 @@ Global
{D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
{9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9} = {264B412F-DB8B-4CF8-A74B-96998B183045}
{1AFB6476-670D-4E80-A464-657E01DFF482} = {557C4636-D7E1-4838-A504-7D19B725EE95}
{1A066C63-64B3-45F8-92FE-664E1CCE8077} = {1AFB6476-670D-4E80-A464-657E01DFF482}
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
{89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
@@ -3130,6 +3151,9 @@ Global
{C32D254F-7597-4CBE-BF74-D922D81CDF29} = {9873BA05-4C41-4819-9283-CF45D795431B}
{02DD46D3-F761-47D9-8894-2D6DA0124650} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
{8E23E173-7127-4A5F-9F93-3049F2B68047} = {929C1324-22E8-4412-A9A8-80E85F3985A5}
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF} = {B5E6F789-0123-4567-8901-23456789ABCD}
{D1234567-8901-2345-6789-ABCDEF012345} = {B5E6F789-0123-4567-8901-23456789ABCD}
{B5E6F789-0123-4567-8901-23456789ABCD} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8} = {538ED0BB-B863-4B20-98CC-BCDF7FA0B68A}
{C0974915-8A1D-4BF0-977B-9587D3807AB7} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
{1D6893CB-BC0C-46A8-A76C-9728706CA51A} = {557C4636-D7E1-4838-A504-7D19B725EE95}

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension" >
<?include $(sys.CURRENTDIR)\Common.wxi?>
<?define PowerDisplayAssetsFiles=?>
<?define PowerDisplayAssetsFilesPath=$(var.BinDir)WinUI3Apps\Assets\PowerDisplay?>
<Fragment>
<!-- Power Display -->
<DirectoryRef Id="WinUI3AppsAssetsFolder">
<Directory Id="PowerDisplayAssetsInstallFolder" Name="PowerDisplay" />
</DirectoryRef>
<DirectoryRef Id="PowerDisplayAssetsInstallFolder" FileSource="$(var.PowerDisplayAssetsFilesPath)">
<!-- Generated by generateFileComponents.ps1 -->
<!--PowerDisplayAssetsFiles_Component_Def-->
</DirectoryRef>
<ComponentGroup Id="PowerDisplayComponentGroup">
<Component Id="RemovePowerDisplayFolder" Guid="B8F2E3A5-72C1-4A2D-9B3F-8E5D7C6A4F9B" Directory="PowerDisplayAssetsInstallFolder" >
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="RemovePowerDisplayFolder" Value="" KeyPath="yes"/>
</RegistryKey>
<RemoveFolder Id="RemoveFolderPowerDisplayAssetsFolder" Directory="PowerDisplayAssetsInstallFolder" On="uninstall"/>
</Component>
</ComponentGroup>
</Fragment>
</Wix>

View File

@@ -65,6 +65,7 @@
<ComponentGroupRef Id="KeyboardManagerComponentGroup" />
<ComponentGroupRef Id="PeekComponentGroup" />
<ComponentGroupRef Id="PowerRenameComponentGroup" />
<ComponentGroupRef Id="PowerDisplayComponentGroup" />
<ComponentGroupRef Id="RegistryPreviewComponentGroup" />
<ComponentGroupRef Id="RunComponentGroup" />
<ComponentGroupRef Id="SettingsComponentGroup" />

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension" >
<?include $(sys.CURRENTDIR)\Common.wxi?>
<?define PowerDisplayAssetsFiles=?>
<?define PowerDisplayAssetsFilesPath=$(var.BinDir)WinUI3Apps\Assets\PowerDisplay?>
<Fragment>
<!-- Power Display -->
<DirectoryRef Id="WinUI3AppsAssetsFolder">
<Directory Id="PowerDisplayAssetsInstallFolder" Name="PowerDisplay" />
</DirectoryRef>
<DirectoryRef Id="PowerDisplayAssetsInstallFolder" FileSource="$(var.PowerDisplayAssetsFilesPath)">
<!-- Generated by generateFileComponents.ps1 -->
<!--PowerDisplayAssetsFiles_Component_Def-->
</DirectoryRef>
<ComponentGroup Id="PowerDisplayComponentGroup">
<Component Id="RemovePowerDisplayFolder" Guid="B8F2E3A5-72C1-4A2D-9B3F-8E5D7C6A4F9B" Directory="PowerDisplayAssetsInstallFolder" >
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="RemovePowerDisplayFolder" Value="" KeyPath="yes"/>
</RegistryKey>
<RemoveFolder Id="RemoveFolderPowerDisplayAssetsFolder" Directory="PowerDisplayAssetsInstallFolder" On="uninstall"/>
</Component>
</ComponentGroup>
</Fragment>
</Wix>

View File

@@ -52,6 +52,7 @@
<ComponentGroupRef Id="KeyboardManagerComponentGroup" />
<ComponentGroupRef Id="PeekComponentGroup" />
<ComponentGroupRef Id="PowerRenameComponentGroup" />
<ComponentGroupRef Id="PowerDisplayComponentGroup" />
<ComponentGroupRef Id="RegistryPreviewComponentGroup" />
<ComponentGroupRef Id="RunComponentGroup" />
<ComponentGroupRef Id="SettingsComponentGroup" />

View File

@@ -42,6 +42,7 @@ namespace Common.UI
NewPlus,
CmdPal,
ZoomIt,
PowerDisplay,
}
private static string SettingsWindowNameToString(SettingsWindow value)
@@ -110,6 +111,8 @@ namespace Common.UI
return "CmdPal";
case SettingsWindow.ZoomIt:
return "ZoomIt";
case SettingsWindow.PowerDisplay:
return "PowerDisplay";
default:
{
return string.Empty;

View File

@@ -28,6 +28,7 @@ namespace ManagedCommon
PowerRename,
PowerLauncher,
PowerAccent,
PowerDisplay,
RegistryPreview,
MeasureTool,
ShortcutGuide,

View File

@@ -195,4 +195,12 @@ namespace winrt::PowerToys::Interop::implementation
{
return CommonSharedConstants::CMDPAL_SHOW_EVENT;
}
hstring Constants::ShowPowerDisplayEvent()
{
return CommonSharedConstants::SHOW_POWER_DISPLAY_EVENT;
}
hstring Constants::TerminatePowerDisplayEvent()
{
return CommonSharedConstants::TERMINATE_POWER_DISPLAY_EVENT;
}
}

View File

@@ -52,6 +52,8 @@ namespace winrt::PowerToys::Interop::implementation
static hstring WorkspacesHotkeyEvent();
static hstring PowerToysRunnerTerminateSettingsEvent();
static hstring ShowCmdPalEvent();
static hstring ShowPowerDisplayEvent();
static hstring TerminatePowerDisplayEvent();
};
}

View File

@@ -131,6 +131,10 @@ namespace CommonSharedConstants
const wchar_t ZOOMIT_REFRESH_SETTINGS_EVENT[] = L"Local\\PowerToysZoomIt-RefreshSettingsEvent-f053a563-d519-4b0d-8152-a54489c13324";
const wchar_t ZOOMIT_EXIT_EVENT[] = L"Local\\PowerToysZoomIt-ExitEvent-36641ce6-df02-4eac-abea-a3fbf9138220";
// Path to the events used by PowerDisplay
const wchar_t SHOW_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ShowEvent-d8a4e0e3-2c5b-4a1c-9e7f-8b3d6c1a2f4e";
const wchar_t TERMINATE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-TerminateEvent-7b9c2e1f-8a5d-4c3e-9f6b-2a1d8c5e3b7a";
// used from quick access window
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";
const wchar_t CMDPAL_EXIT_EVENT[] = L"Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd";

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

View File

@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace PowerDisplay.Converters
{
/// <summary>
/// Converts boolean values to Visibility
/// </summary>
public partial class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is bool boolValue)
{
return boolValue ? Visibility.Visible : Visibility.Collapsed;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
if (value is Visibility visibility)
{
return visibility == Visibility.Visible;
}
return false;
}
}
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace PowerDisplay.Converters
{
/// <summary>
/// Converts boolean values to Visibility (inverted)
/// </summary>
public partial class InverseBoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is bool boolValue)
{
return boolValue ? Visibility.Collapsed : Visibility.Visible;
}
return Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
if (value is Visibility visibility)
{
return visibility != Visibility.Visible;
}
return true;
}
}
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace PowerDisplay.Converters
{
/// <summary>
/// Special converter for "No monitors" visibility
/// Shows when initialized but has no monitors
/// </summary>
public partial class NoMonitorsVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
// This would need access to both IsInitialized and HasMonitors
// For simplicity, we'll handle this in the ViewModel
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,161 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using PowerDisplay.Core.Models;
using Monitor = PowerDisplay.Core.Models.Monitor;
namespace PowerDisplay.Core.Interfaces
{
/// <summary>
/// Monitor controller interface
/// </summary>
public interface IMonitorController
{
/// <summary>
/// Controller name
/// </summary>
string Name { get; }
/// <summary>
/// Supported monitor type
/// </summary>
MonitorType SupportedType { get; }
/// <summary>
/// Checks whether the specified monitor can be controlled
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Whether the monitor can be controlled</returns>
Task<bool> CanControlMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default);
/// <summary>
/// Gets monitor brightness
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Brightness information</returns>
Task<BrightnessInfo> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default);
/// <summary>
/// Sets monitor brightness
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="brightness">Brightness value (0-100)</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Operation result</returns>
Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default);
/// <summary>
/// Discovers supported monitors
/// </summary>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>List of monitors</returns>
Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Validates monitor connection status
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Whether the monitor is connected</returns>
Task<bool> ValidateConnectionAsync(Monitor monitor, CancellationToken cancellationToken = default);
/// <summary>
/// Releases resources
/// </summary>
void Dispose();
}
/// <summary>
/// Extended monitor controller interface (supports additional features)
/// </summary>
public interface IExtendedMonitorController : IMonitorController
{
/// <summary>
/// Gets monitor contrast
/// </summary>
Task<BrightnessInfo> GetContrastAsync(Monitor monitor, CancellationToken cancellationToken = default);
/// <summary>
/// Sets monitor contrast
/// </summary>
Task<MonitorOperationResult> SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default);
/// <summary>
/// Gets monitor volume
/// </summary>
Task<BrightnessInfo> GetVolumeAsync(Monitor monitor, CancellationToken cancellationToken = default);
/// <summary>
/// Sets monitor volume
/// </summary>
Task<MonitorOperationResult> SetVolumeAsync(Monitor monitor, int volume, CancellationToken cancellationToken = default);
/// <summary>
/// Gets monitor capabilities string (DDC/CI)
/// </summary>
Task<string> GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default);
/// <summary>
/// Saves current settings to monitor
/// </summary>
Task<MonitorOperationResult> SaveCurrentSettingsAsync(Monitor monitor, CancellationToken cancellationToken = default);
}
/// <summary>
/// Monitor manager interface
/// </summary>
public interface IMonitorManager
{
/// <summary>
/// Currently detected monitors list
/// </summary>
IReadOnlyList<Monitor> Monitors { get; }
/// <summary>
/// Monitor list changed event
/// </summary>
event EventHandler<MonitorListChangedEventArgs>? MonitorsChanged;
/// <summary>
/// Monitor status changed event
/// </summary>
event EventHandler<MonitorStatusChangedEventArgs>? MonitorStatusChanged;
/// <summary>
/// Discovers all monitors
/// </summary>
Task<IReadOnlyList<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Gets brightness of specified monitor
/// </summary>
Task<BrightnessInfo> GetBrightnessAsync(string monitorId, CancellationToken cancellationToken = default);
/// <summary>
/// Sets brightness of specified monitor
/// </summary>
Task<MonitorOperationResult> SetBrightnessAsync(string monitorId, int brightness, CancellationToken cancellationToken = default);
/// <summary>
/// Sets brightness of all monitors
/// </summary>
Task<IEnumerable<MonitorOperationResult>> SetAllBrightnessAsync(int brightness, CancellationToken cancellationToken = default);
/// <summary>
/// Refreshes monitor status
/// </summary>
Task RefreshMonitorStatusAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Gets monitor by ID
/// </summary>
Monitor? GetMonitor(string monitorId);
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using PowerDisplay.Core.Models;
namespace PowerDisplay.Core.Interfaces
{
/// <summary>
/// Monitor list changed event arguments
/// </summary>
public class MonitorListChangedEventArgs : EventArgs
{
public IReadOnlyList<Monitor> AddedMonitors { get; }
public IReadOnlyList<Monitor> RemovedMonitors { get; }
public IReadOnlyList<Monitor> AllMonitors { get; }
public MonitorListChangedEventArgs(
IReadOnlyList<Monitor> addedMonitors,
IReadOnlyList<Monitor> removedMonitors,
IReadOnlyList<Monitor> allMonitors)
{
AddedMonitors = addedMonitors;
RemovedMonitors = removedMonitors;
AllMonitors = allMonitors;
}
}
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using PowerDisplay.Core.Models;
namespace PowerDisplay.Core.Interfaces
{
/// <summary>
/// Monitor status changed event arguments
/// </summary>
public class MonitorStatusChangedEventArgs : EventArgs
{
public Monitor Monitor { get; }
public int? OldBrightness { get; }
public int NewBrightness { get; }
public bool? OldAvailability { get; }
public bool NewAvailability { get; }
public string Message { get; }
public ChangeType Type { get; }
public enum ChangeType
{
Brightness,
Contrast,
Volume,
ColorTemperature,
Availability,
General
}
public MonitorStatusChangedEventArgs(
Monitor monitor,
int? oldBrightness,
int newBrightness,
bool? oldAvailability,
bool newAvailability)
{
Monitor = monitor;
OldBrightness = oldBrightness;
NewBrightness = newBrightness;
OldAvailability = oldAvailability;
NewAvailability = newAvailability;
Message = $"Brightness changed from {oldBrightness} to {newBrightness}";
Type = ChangeType.Brightness;
}
public MonitorStatusChangedEventArgs(
Monitor monitor,
string message,
ChangeType changeType)
{
Monitor = monitor;
Message = message;
Type = changeType;
// Set defaults for compatibility
OldBrightness = null;
NewBrightness = monitor.CurrentBrightness;
OldAvailability = null;
NewAvailability = monitor.IsAvailable;
}
}
}

View File

@@ -0,0 +1,88 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace PowerDisplay.Core.Models
{
/// <summary>
/// Brightness information structure
/// </summary>
public readonly struct BrightnessInfo
{
/// <summary>
/// Current brightness value
/// </summary>
public int Current { get; }
/// <summary>
/// Minimum brightness value
/// </summary>
public int Minimum { get; }
/// <summary>
/// Maximum brightness value
/// </summary>
public int Maximum { get; }
/// <summary>
/// Whether the brightness information is valid
/// </summary>
public bool IsValid { get; }
/// <summary>
/// Timestamp when the brightness information was obtained
/// </summary>
public DateTime Timestamp { get; }
public BrightnessInfo(int current, int minimum, int maximum)
{
Current = current;
Minimum = minimum;
Maximum = maximum;
IsValid = current >= minimum && current <= maximum && maximum > minimum;
Timestamp = DateTime.Now;
}
public BrightnessInfo(int current, int maximum)
: this(current, 0, maximum)
{
}
/// <summary>
/// Creates invalid brightness information
/// </summary>
public static BrightnessInfo Invalid => new(-1, -1, -1);
/// <summary>
/// Converts brightness value to percentage (0-100)
/// </summary>
public int ToPercentage()
{
if (!IsValid || Maximum == Minimum)
{
return 0;
}
return (int)Math.Round((double)(Current - Minimum) * 100 / (Maximum - Minimum));
}
/// <summary>
/// Creates brightness value from percentage
/// </summary>
public int FromPercentage(int percentage)
{
if (!IsValid)
{
return -1;
}
percentage = Math.Clamp(percentage, 0, 100);
return Minimum + (int)Math.Round((double)(Maximum - Minimum) * percentage / 100);
}
public override string ToString()
{
return IsValid ? $"{Current}/{Maximum} ({ToPercentage()}%)" : "Invalid";
}
}
}

View File

@@ -0,0 +1,253 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace PowerDisplay.Core.Models
{
/// <summary>
/// Monitor model that implements property change notification
/// </summary>
public class Monitor : INotifyPropertyChanged
{
private int _currentBrightness;
private int _currentColorTemperature = 6500;
private bool _isAvailable = true;
/// <summary>
/// Unique identifier (based on hardware ID)
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Hardware ID (EDID format like GSM5C6D)
/// </summary>
public string HardwareId { get; set; } = string.Empty;
/// <summary>
/// Display name
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Monitor type
/// </summary>
public MonitorType Type { get; set; } = MonitorType.Unknown;
/// <summary>
/// Current brightness (0-100)
/// </summary>
public int CurrentBrightness
{
get => _currentBrightness;
set
{
if (_currentBrightness != value)
{
_currentBrightness = Math.Clamp(value, MinBrightness, MaxBrightness);
OnPropertyChanged();
}
}
}
/// <summary>
/// Minimum brightness value
/// </summary>
public int MinBrightness { get; set; }
/// <summary>
/// Maximum brightness value
/// </summary>
public int MaxBrightness { get; set; } = 100;
/// <summary>
/// Current color temperature (2000-10000K)
/// </summary>
public int CurrentColorTemperature
{
get => _currentColorTemperature;
set
{
if (_currentColorTemperature != value)
{
_currentColorTemperature = Math.Clamp(value, MinColorTemperature, MaxColorTemperature);
OnPropertyChanged();
}
}
}
/// <summary>
/// Minimum color temperature value
/// </summary>
public int MinColorTemperature { get; set; } = 2000;
/// <summary>
/// Maximum color temperature value
/// </summary>
public int MaxColorTemperature { get; set; } = 10000;
/// <summary>
/// Whether supports color temperature adjustment
/// </summary>
public bool SupportsColorTemperature { get; set; } = true;
/// <summary>
/// Whether supports contrast adjustment
/// </summary>
public bool SupportsContrast => Capabilities.HasFlag(MonitorCapabilities.Contrast);
/// <summary>
/// Whether supports volume adjustment (for audio-capable monitors)
/// </summary>
public bool SupportsVolume => Capabilities.HasFlag(MonitorCapabilities.Volume);
private int _currentContrast = 50;
private int _currentVolume = 50;
/// <summary>
/// Current contrast (0-100)
/// </summary>
public int CurrentContrast
{
get => _currentContrast;
set
{
if (_currentContrast != value)
{
_currentContrast = Math.Clamp(value, MinContrast, MaxContrast);
OnPropertyChanged();
}
}
}
/// <summary>
/// Minimum contrast value
/// </summary>
public int MinContrast { get; set; } = 0;
/// <summary>
/// Maximum contrast value
/// </summary>
public int MaxContrast { get; set; } = 100;
/// <summary>
/// Current volume (0-100)
/// </summary>
public int CurrentVolume
{
get => _currentVolume;
set
{
if (_currentVolume != value)
{
_currentVolume = Math.Clamp(value, MinVolume, MaxVolume);
OnPropertyChanged();
}
}
}
/// <summary>
/// Minimum volume value
/// </summary>
public int MinVolume { get; set; } = 0;
/// <summary>
/// Maximum volume value
/// </summary>
public int MaxVolume { get; set; } = 100;
/// <summary>
/// Whether available/online
/// </summary>
public bool IsAvailable
{
get => _isAvailable;
set
{
if (_isAvailable != value)
{
_isAvailable = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Physical monitor handle (for DDC/CI)
/// </summary>
public IntPtr Handle { get; set; } = IntPtr.Zero;
/// <summary>
/// Device path (for identification)
/// </summary>
public string DevicePath { get; set; } = string.Empty;
/// <summary>
/// Device key - unique identifier part of device path (like Twinkle Tray's deviceKey)
/// </summary>
public string DeviceKey { get; set; } = string.Empty;
/// <summary>
/// Full device ID path (like Twinkle Tray's deviceID)
/// </summary>
public string DeviceID { get; set; } = string.Empty;
/// <summary>
/// Instance name (used by WMI)
/// </summary>
public string InstanceName { get; set; } = string.Empty;
/// <summary>
/// Manufacturer information
/// </summary>
public string Manufacturer { get; set; } = string.Empty;
/// <summary>
/// Connection type (HDMI, DP, VGA, etc.)
/// </summary>
public string ConnectionType { get; set; } = string.Empty;
/// <summary>
/// Communication method (DDC/CI, WMI, HDR API, etc.)
/// </summary>
public string CommunicationMethod { get; set; } = string.Empty;
/// <summary>
/// Supported control methods
/// </summary>
public MonitorCapabilities Capabilities { get; set; } = MonitorCapabilities.None;
/// <summary>
/// Last update time
/// </summary>
public DateTime LastUpdate { get; set; } = DateTime.Now;
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public override string ToString()
{
return $"{Name} ({Type}) - {CurrentBrightness}%";
}
/// <summary>
/// Update monitor status
/// </summary>
public void UpdateStatus(int brightness, bool isAvailable = true)
{
IsAvailable = isAvailable;
if (isAvailable)
{
CurrentBrightness = brightness;
LastUpdate = DateTime.Now;
}
}
}
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace PowerDisplay.Core.Models
{
/// <summary>
/// Monitor control capabilities flags
/// </summary>
[Flags]
public enum MonitorCapabilities
{
None = 0,
/// <summary>
/// Supports brightness control
/// </summary>
Brightness = 1 << 0,
/// <summary>
/// Supports contrast control
/// </summary>
Contrast = 1 << 1,
/// <summary>
/// Supports DDC/CI protocol
/// </summary>
DdcCi = 1 << 2,
/// <summary>
/// Supports WMI control
/// </summary>
Wmi = 1 << 3,
/// <summary>
/// Supports HDR
/// </summary>
Hdr = 1 << 4,
/// <summary>
/// Supports high-level monitor API
/// </summary>
HighLevel = 1 << 5,
/// <summary>
/// Supports volume control
/// </summary>
Volume = 1 << 6,
}
}

View File

@@ -0,0 +1,58 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace PowerDisplay.Core.Models
{
/// <summary>
/// Monitor operation result
/// </summary>
public readonly struct MonitorOperationResult
{
/// <summary>
/// Whether the operation was successful
/// </summary>
public bool IsSuccess { get; }
/// <summary>
/// Error message
/// </summary>
public string? ErrorMessage { get; }
/// <summary>
/// System error code
/// </summary>
public int? ErrorCode { get; }
/// <summary>
/// Operation timestamp
/// </summary>
public DateTime Timestamp { get; }
private MonitorOperationResult(bool isSuccess, string? errorMessage = null, int? errorCode = null)
{
IsSuccess = isSuccess;
ErrorMessage = errorMessage;
ErrorCode = errorCode;
Timestamp = DateTime.Now;
}
/// <summary>
/// Creates a successful result
/// </summary>
public static MonitorOperationResult Success() => new(true);
/// <summary>
/// Creates a failed result
/// </summary>
public static MonitorOperationResult Failure(string errorMessage, int? errorCode = null)
=> new(false, errorMessage, errorCode);
public override string ToString()
{
return IsSuccess ? "Success" : $"Failed: {ErrorMessage}";
}
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace PowerDisplay.Core.Models
{
/// <summary>
/// Monitor type enumeration
/// </summary>
public enum MonitorType
{
/// <summary>
/// Unknown type
/// </summary>
Unknown,
/// <summary>
/// Internal display (laptop screen, controlled via WMI)
/// </summary>
Internal,
/// <summary>
/// External display (controlled via DDC/CI)
/// </summary>
External,
/// <summary>
/// HDR display (controlled via Display Config API)
/// </summary>
HDR,
}
}

View File

@@ -0,0 +1,578 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using PowerDisplay.Core.Interfaces;
using PowerDisplay.Core.Models;
using PowerDisplay.Native.DDC;
using PowerDisplay.Native.WMI;
using Monitor = PowerDisplay.Core.Models.Monitor;
namespace PowerDisplay.Core
{
/// <summary>
/// Monitor manager for unified control of all monitors
/// </summary>
public class MonitorManager : IMonitorManager, IDisposable
{
private readonly List<Monitor> _monitors = new();
private readonly List<IMonitorController> _controllers = new();
private readonly SemaphoreSlim _discoveryLock = new(1, 1);
private readonly Timer _refreshTimer;
private bool _disposed;
public IReadOnlyList<Monitor> Monitors => _monitors.AsReadOnly();
public event EventHandler<MonitorListChangedEventArgs>? MonitorsChanged;
public event EventHandler<MonitorStatusChangedEventArgs>? MonitorStatusChanged;
public MonitorManager()
{
// Initialize controllers
InitializeControllers();
// Set up periodic refresh timer (check every 30 seconds)
_refreshTimer = new Timer(async _ => await RefreshMonitorStatusAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(30));
}
/// <summary>
/// Initialize controllers
/// </summary>
private void InitializeControllers()
{
try
{
// DDC/CI controller (external monitors)
_controllers.Add(new DdcCiController());
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to initialize DDC/CI controller: {ex.Message}");
}
try
{
// WMI controller (internal monitors)
// First check if WMI is available
if (WmiController.IsWmiAvailable())
{
_controllers.Add(new WmiController());
}
else
{
Logger.LogInfo("WMI brightness control not available on this system");
}
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to initialize WMI controller: {ex.Message}");
}
}
/// <summary>
/// Discover all monitors
/// </summary>
public async Task<IReadOnlyList<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
{
await _discoveryLock.WaitAsync(cancellationToken);
try
{
var oldMonitors = _monitors.ToList();
var newMonitors = new List<Monitor>();
// Discover monitors supported by all controllers in parallel
var discoveryTasks = _controllers.Select(async controller =>
{
try
{
var monitors = await controller.DiscoverMonitorsAsync(cancellationToken);
return (Controller: controller, Monitors: monitors.ToList());
}
catch (Exception)
{
// If a controller fails, return empty list
return (Controller: controller, Monitors: new List<Monitor>());
}
});
var results = await Task.WhenAll(discoveryTasks);
// Collect all discovered monitors
foreach (var (controller, monitors) in results)
{
foreach (var monitor in monitors)
{
// Verify if monitor can be controlled
if (await controller.CanControlMonitorAsync(monitor, cancellationToken))
{
// Get current brightness
try
{
var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken);
if (brightnessInfo.IsValid)
{
monitor.CurrentBrightness = brightnessInfo.ToPercentage();
monitor.MinBrightness = brightnessInfo.Minimum;
monitor.MaxBrightness = brightnessInfo.Maximum;
}
}
catch
{
// If unable to get brightness, use default values
}
newMonitors.Add(monitor);
}
}
}
// Update monitor list
_monitors.Clear();
_monitors.AddRange(newMonitors);
// Trigger change events
var addedMonitors = newMonitors.Where(m => !oldMonitors.Any(o => o.Id == m.Id)).ToList();
var removedMonitors = oldMonitors.Where(o => !newMonitors.Any(m => m.Id == o.Id)).ToList();
if (addedMonitors.Count > 0 || removedMonitors.Count > 0)
{
MonitorsChanged?.Invoke(this, new MonitorListChangedEventArgs(
addedMonitors.AsReadOnly(),
removedMonitors.AsReadOnly(),
_monitors.AsReadOnly()));
}
return _monitors.AsReadOnly();
}
finally
{
_discoveryLock.Release();
}
}
/// <summary>
/// Get brightness of the specified monitor
/// </summary>
public async Task<BrightnessInfo> GetBrightnessAsync(string monitorId, CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
return BrightnessInfo.Invalid;
}
var controller = GetControllerForMonitor(monitor);
if (controller == null)
{
return BrightnessInfo.Invalid;
}
try
{
var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken);
// Update cached brightness value
if (brightnessInfo.IsValid)
{
var oldBrightness = monitor.CurrentBrightness;
monitor.UpdateStatus(brightnessInfo.ToPercentage(), true);
// Trigger status change event
if (oldBrightness != monitor.CurrentBrightness)
{
MonitorStatusChanged?.Invoke(this, new MonitorStatusChangedEventArgs(
monitor, oldBrightness, monitor.CurrentBrightness, true, true));
}
}
return brightnessInfo;
}
catch
{
// Mark monitor as unavailable
monitor.IsAvailable = false;
return BrightnessInfo.Invalid;
}
}
/// <summary>
/// Set brightness of the specified monitor
/// </summary>
public async Task<MonitorOperationResult> SetBrightnessAsync(string monitorId, int brightness, CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
return MonitorOperationResult.Failure("Monitor not found");
}
var controller = GetControllerForMonitor(monitor);
if (controller == null)
{
return MonitorOperationResult.Failure("No controller available for this monitor");
}
try
{
var oldBrightness = monitor.CurrentBrightness;
var result = await controller.SetBrightnessAsync(monitor, brightness, cancellationToken);
if (result.IsSuccess)
{
// Update monitor status
monitor.UpdateStatus(brightness, true);
// Trigger status change event
MonitorStatusChanged?.Invoke(this, new MonitorStatusChangedEventArgs(
monitor, oldBrightness, brightness, true, true));
}
else
{
// If setting fails, monitor may be unavailable
monitor.IsAvailable = false;
}
return result;
}
catch (Exception ex)
{
monitor.IsAvailable = false;
return MonitorOperationResult.Failure($"Exception setting brightness: {ex.Message}");
}
}
/// <summary>
/// Set brightness of all monitors
/// </summary>
public async Task<IEnumerable<MonitorOperationResult>> SetAllBrightnessAsync(int brightness, CancellationToken cancellationToken = default)
{
var tasks = _monitors
.Where(m => m.IsAvailable)
.Select(async monitor =>
{
try
{
return await SetBrightnessAsync(monitor.Id, brightness, cancellationToken);
}
catch (Exception ex)
{
return MonitorOperationResult.Failure($"Failed to set brightness for {monitor.Name}: {ex.Message}");
}
});
return await Task.WhenAll(tasks);
}
/// <summary>
/// Set contrast of the specified monitor
/// </summary>
public async Task<MonitorOperationResult> SetContrastAsync(string monitorId, int contrast, CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
return MonitorOperationResult.Failure("Monitor not found");
}
var controller = GetControllerForMonitor(monitor) as IExtendedMonitorController;
if (controller == null)
{
return MonitorOperationResult.Failure("No extended controller available for this monitor");
}
try
{
var oldContrast = monitor.CurrentContrast;
var result = await controller.SetContrastAsync(monitor, contrast, cancellationToken);
if (result.IsSuccess)
{
monitor.CurrentContrast = contrast;
monitor.LastUpdate = DateTime.Now;
}
else
{
monitor.IsAvailable = false;
}
return result;
}
catch (Exception ex)
{
return MonitorOperationResult.Failure($"Exception setting contrast: {ex.Message}");
}
}
/// <summary>
/// Set volume of the specified monitor
/// </summary>
public async Task<MonitorOperationResult> SetVolumeAsync(string monitorId, int volume, CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
return MonitorOperationResult.Failure("Monitor not found");
}
var controller = GetControllerForMonitor(monitor) as IExtendedMonitorController;
if (controller == null)
{
return MonitorOperationResult.Failure("No extended controller available for this monitor");
}
try
{
var oldVolume = monitor.CurrentVolume;
var result = await controller.SetVolumeAsync(monitor, volume, cancellationToken);
if (result.IsSuccess)
{
monitor.CurrentVolume = volume;
monitor.LastUpdate = DateTime.Now;
}
else
{
monitor.IsAvailable = false;
}
return result;
}
catch (Exception ex)
{
return MonitorOperationResult.Failure($"Exception setting volume: {ex.Message}");
}
}
/// <summary>
/// Get monitor color temperature
/// </summary>
public async Task<BrightnessInfo> GetColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
return BrightnessInfo.Invalid;
}
var controller = GetControllerForMonitor(monitor) as DdcCiController;
if (controller == null)
{
return BrightnessInfo.Invalid;
}
try
{
return await controller.GetColorTemperatureAsync(monitor, cancellationToken);
}
catch (Exception)
{
return BrightnessInfo.Invalid;
}
}
/// <summary>
/// Set monitor color temperature
/// </summary>
public async Task<MonitorOperationResult> SetColorTemperatureAsync(string monitorId, int colorTemperature, CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
return MonitorOperationResult.Failure("Monitor not found");
}
var controller = GetControllerForMonitor(monitor) as DdcCiController;
if (controller == null)
{
return MonitorOperationResult.Failure("DDC/CI controller not available for this monitor");
}
try
{
var oldTemperature = monitor.CurrentColorTemperature;
var result = await controller.SetColorTemperatureAsync(monitor, colorTemperature, cancellationToken);
if (result.IsSuccess)
{
monitor.CurrentColorTemperature = colorTemperature;
monitor.LastUpdate = DateTime.Now;
// Trigger status change event
MonitorStatusChanged?.Invoke(this, new MonitorStatusChangedEventArgs(
monitor,
$"Color temperature changed from {oldTemperature}K to {colorTemperature}K",
MonitorStatusChangedEventArgs.ChangeType.ColorTemperature
));
}
else
{
monitor.IsAvailable = false;
}
return result;
}
catch (Exception ex)
{
return MonitorOperationResult.Failure($"Exception setting color temperature: {ex.Message}");
}
}
/// <summary>
/// Initialize color temperature for a monitor (async operation)
/// </summary>
public async Task InitializeColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
{
try
{
var tempInfo = await GetColorTemperatureAsync(monitorId, cancellationToken);
if (tempInfo.IsValid)
{
var monitor = GetMonitor(monitorId);
if (monitor != null)
{
// Convert VCP value to approximate Kelvin temperature
// This is a rough mapping - actual values depend on monitor implementation
var kelvin = ConvertVcpValueToKelvin(tempInfo.Current, tempInfo.Maximum);
monitor.CurrentColorTemperature = kelvin;
Logger.LogInfo($"Initialized color temperature for {monitorId}: {kelvin}K (VCP: {tempInfo.Current}/{tempInfo.Maximum})");
}
}
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to initialize color temperature for {monitorId}: {ex.Message}");
}
}
/// <summary>
/// Convert VCP value to approximate Kelvin temperature
/// </summary>
private static int ConvertVcpValueToKelvin(int vcpValue, int maxVcpValue)
{
// Standard color temperature range mapping
const int minKelvin = 2000; // Warm
const int maxKelvin = 10000; // Cool
// Normalize VCP value to 0-1 range
double normalizedVcp = maxVcpValue > 0 ? (double)vcpValue / maxVcpValue : 0.5;
// Map to Kelvin range
int kelvin = (int)(minKelvin + (normalizedVcp * (maxKelvin - minKelvin)));
return Math.Clamp(kelvin, minKelvin, maxKelvin);
}
/// <summary>
/// Refresh monitor status
/// </summary>
public async Task RefreshMonitorStatusAsync(CancellationToken cancellationToken = default)
{
var tasks = _monitors.Select(async monitor =>
{
try
{
var controller = GetControllerForMonitor(monitor);
if (controller == null)
{
return;
}
// Validate connection status
var isConnected = await controller.ValidateConnectionAsync(monitor, cancellationToken);
var oldAvailability = monitor.IsAvailable;
if (isConnected)
{
// 获取当前亮度
var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken);
if (brightnessInfo.IsValid)
{
var oldBrightness = monitor.CurrentBrightness;
monitor.UpdateStatus(brightnessInfo.ToPercentage(), true);
// Trigger status change event
if (oldBrightness != monitor.CurrentBrightness || oldAvailability != monitor.IsAvailable)
{
MonitorStatusChanged?.Invoke(this, new MonitorStatusChangedEventArgs(
monitor, oldBrightness, monitor.CurrentBrightness, oldAvailability, monitor.IsAvailable));
}
}
else
{
monitor.IsAvailable = false;
}
}
else
{
monitor.IsAvailable = false;
// Trigger availability change event
if (oldAvailability != monitor.IsAvailable)
{
MonitorStatusChanged?.Invoke(this, new MonitorStatusChangedEventArgs(
monitor, monitor.CurrentBrightness, monitor.CurrentBrightness, oldAvailability, monitor.IsAvailable));
}
}
}
catch
{
// Refresh failed, mark as unavailable
monitor.IsAvailable = false;
}
});
await Task.WhenAll(tasks);
}
/// <summary>
/// Get monitor by ID
/// </summary>
public Monitor? GetMonitor(string monitorId)
{
return _monitors.FirstOrDefault(m => m.Id == monitorId);
}
/// <summary>
/// Get controller for the monitor
/// </summary>
private IMonitorController? GetControllerForMonitor(Monitor monitor)
{
return _controllers.FirstOrDefault(c => c.SupportedType == monitor.Type);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed && disposing)
{
_refreshTimer?.Dispose();
_discoveryLock?.Dispose();
// Release all controllers
foreach (var controller in _controllers)
{
controller?.Dispose();
}
_controllers.Clear();
_monitors.Clear();
_disposed = true;
}
}
}
}

View File

@@ -0,0 +1,114 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
namespace PowerDisplay.Helpers
{
/// <summary>
/// Serializes monitor property updates to prevent race conditions
/// Simple approach: one operation at a time, newest replaces pending
/// </summary>
public class MonitorPropertyManager : IDisposable
{
private readonly SemaphoreSlim _operationSemaphore = new(1, 1);
private int _pendingValue = -1; // Value waiting to be executed
private bool _hasPendingValue = false;
private Task? _currentTask;
private readonly string _monitorId;
private readonly string _propertyName;
public MonitorPropertyManager(string monitorId, string propertyName)
{
_monitorId = monitorId;
_propertyName = propertyName;
}
/// <summary>
/// Queue a property update - replaces any pending update
/// </summary>
public void QueueUpdate(int newValue, Func<int, CancellationToken, Task> updateAction)
{
lock (this)
{
// Always update the pending value (newest wins)
_pendingValue = newValue;
_hasPendingValue = true;
// If no operation is currently running, start one
if (_currentTask == null || _currentTask.IsCompleted)
{
_currentTask = ExecuteUpdatesAsync(updateAction);
}
}
}
private async Task ExecuteUpdatesAsync(Func<int, CancellationToken, Task> updateAction)
{
while (true)
{
int valueToUpdate;
// Get the next value to update
lock (this)
{
if (!_hasPendingValue)
{
// No more updates pending
break;
}
valueToUpdate = _pendingValue;
_hasPendingValue = false;
}
// Execute the hardware update
try
{
await _operationSemaphore.WaitAsync();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await updateAction(valueToUpdate, cts.Token);
Logger.LogDebug($"[{_monitorId}] {_propertyName} updated to {valueToUpdate}");
}
catch (Exception ex)
{
Logger.LogError($"[{_monitorId}] Failed to update {_propertyName} to {valueToUpdate}: {ex.Message}");
}
finally
{
_operationSemaphore.Release();
}
}
}
/// <summary>
/// Wait for all pending updates to complete
/// </summary>
public async Task FlushAsync()
{
var currentTask = _currentTask;
if (currentTask != null && !currentTask.IsCompleted)
{
try
{
await currentTask;
}
catch
{
// Ignore errors during flush
}
}
}
public void Dispose()
{
_operationSemaphore?.Dispose();
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.Windows.ApplicationModel.Resources;
namespace PowerDisplay.Helpers
{
public static class ResourceLoaderInstance
{
public static ResourceLoader ResourceLoader { get; private set; }
static ResourceLoaderInstance()
{
ResourceLoader = new ResourceLoader("PowerToys.PowerDisplay.pri");
}
}
}

View File

@@ -0,0 +1,347 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
namespace PowerDisplay.Helpers
{
/// <summary>
/// Settings manager with periodic save mechanism (write-only during runtime)
/// </summary>
public class SettingsManager : IDisposable
{
private readonly ISettingsUtils _settingsUtils;
private readonly SemaphoreSlim _fileAccessSemaphore = new(1, 1);
private readonly Timer _periodicSaveTimer;
private readonly ConcurrentDictionary<string, MonitorSettings> _pendingSettings = new();
private const string MODULE_NAME = "PowerDisplay";
private const int PERIODIC_SAVE_INTERVAL_MS = 1000; // Check every 1 second
private bool _disposed;
private bool _hasPendingChanges = false;
/// <summary>
/// Represents all settings for a single monitor
/// </summary>
private class MonitorSettings
{
public int Brightness { get; set; }
public int ColorTemperature { get; set; }
public int Contrast { get; set; }
public int Volume { get; set; }
public bool IsDirty { get; set; } = false;
}
public SettingsManager(ISettingsUtils settingsUtils)
{
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
// Start periodic timer that checks every second
_periodicSaveTimer = new Timer(CheckAndSavePendingChanges, null, PERIODIC_SAVE_INTERVAL_MS, PERIODIC_SAVE_INTERVAL_MS);
}
/// <summary>
/// Update a setting value in memory (will be saved on next periodic check)
/// </summary>
public void QueueSettingChange(string monitorId, string property, object value)
{
try
{
var intValue = Convert.ToInt32(value);
// Get or create monitor settings
var settings = _pendingSettings.GetOrAdd(monitorId, _ => new MonitorSettings());
// Update the specific property
switch (property)
{
case "Brightness":
settings.Brightness = intValue;
break;
case "ColorTemperature":
settings.ColorTemperature = intValue;
break;
case "Contrast":
settings.Contrast = intValue;
break;
case "Volume":
settings.Volume = intValue;
break;
default:
Logger.LogWarning($"Unknown property: {property}");
return;
}
settings.IsDirty = true;
_hasPendingChanges = true;
Logger.LogTrace($"[Queue] Updated {property}={intValue} for monitor '{monitorId}' in memory");
}
catch (Exception ex)
{
Logger.LogError($"Failed to queue setting change: {ex.Message}");
}
}
/// <summary>
/// Periodic check (every 1 second) - save if there are pending changes
/// </summary>
private async void CheckAndSavePendingChanges(object? state)
{
// Quick check without lock - if no changes, skip
if (_disposed || !_hasPendingChanges)
{
return;
}
await _fileAccessSemaphore.WaitAsync();
try
{
// Double check after acquiring lock
if (!_hasPendingChanges)
{
return;
}
// Load current settings
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(MODULE_NAME);
if (settings.Properties.SavedMonitorSettings == null)
{
settings.Properties.SavedMonitorSettings = new Dictionary<string, MonitorSavedSettings>();
}
int changeCount = 0;
var now = DateTime.Now;
// Apply all dirty monitor settings
foreach (var kvp in _pendingSettings)
{
var monitorId = kvp.Key;
var monitorSettings = kvp.Value;
if (!monitorSettings.IsDirty)
{
continue;
}
// Get or create saved settings for this monitor
if (!settings.Properties.SavedMonitorSettings.ContainsKey(monitorId))
{
settings.Properties.SavedMonitorSettings[monitorId] = new MonitorSavedSettings();
}
var savedSettings = settings.Properties.SavedMonitorSettings[monitorId];
// Update all properties
savedSettings.Brightness = monitorSettings.Brightness;
savedSettings.ColorTemperature = monitorSettings.ColorTemperature;
savedSettings.Contrast = monitorSettings.Contrast;
savedSettings.Volume = monitorSettings.Volume;
savedSettings.LastUpdated = now;
// Mark as clean
monitorSettings.IsDirty = false;
changeCount++;
}
// If we saved anything, write to file
if (changeCount > 0)
{
_settingsUtils.SaveSettings(settings.ToJsonString(), MODULE_NAME);
Logger.LogInfo($"[Periodic Save] Saved settings for {changeCount} monitor(s)");
}
// Reset flag
_hasPendingChanges = false;
}
catch (Exception ex)
{
Logger.LogError($"Failed to save pending changes: {ex.Message}");
}
finally
{
_fileAccessSemaphore.Release();
}
}
/// <summary>
/// Force immediate save of all pending changes
/// </summary>
public async Task FlushPendingChangesAsync()
{
// Temporarily stop the timer to prevent concurrent execution
_periodicSaveTimer.Change(Timeout.Infinite, Timeout.Infinite);
try
{
// Trigger immediate save
CheckAndSavePendingChanges(null);
// Give it a moment to complete
await Task.Delay(100);
}
finally
{
// Restart the periodic timer
_periodicSaveTimer.Change(PERIODIC_SAVE_INTERVAL_MS, PERIODIC_SAVE_INTERVAL_MS);
}
}
/// <summary>
/// Thread-safe method to save monitor info
/// </summary>
public async Task SaveMonitorInfoAsync(IReadOnlyList<PowerDisplay.Core.Models.Monitor> monitors)
{
await _fileAccessSemaphore.WaitAsync();
try
{
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(MODULE_NAME);
var monitorInfoList = monitors.Select(monitor =>
{
var existingMonitor = settings.Properties.Monitors.FirstOrDefault(m =>
m.HardwareId == monitor.HardwareId || m.InternalName == GetInternalName(monitor));
var monitorInfo = new MonitorInfo(
monitor.Name,
GetInternalName(monitor),
monitor.HardwareId,
GetCommunicationMethod(monitor),
GetMonitorType(monitor),
monitor.CurrentBrightness,
monitor.CurrentColorTemperature
);
if (existingMonitor != null)
{
monitorInfo.EnableColorTemperature = existingMonitor.EnableColorTemperature;
monitorInfo.EnableContrast = existingMonitor.EnableContrast;
monitorInfo.EnableVolume = existingMonitor.EnableVolume;
monitorInfo.IsHidden = existingMonitor.IsHidden;
}
return monitorInfo;
}).ToList();
settings.Properties.Monitors = monitorInfoList;
_settingsUtils.SaveSettings(settings.ToJsonString(), MODULE_NAME);
Logger.LogInfo($"Synchronized save of monitor info for {monitors.Count} monitors");
}
catch (Exception ex)
{
Logger.LogError($"Failed to save monitor info: {ex.Message}");
}
finally
{
_fileAccessSemaphore.Release();
}
}
private string GetInternalName(PowerDisplay.Core.Models.Monitor monitor)
{
if (monitor.Type == PowerDisplay.Core.Models.MonitorType.Internal)
{
return "Internal Display";
}
var id = monitor.Id;
if (id.StartsWith("DDC_"))
{
return id.Substring(4);
}
return id;
}
private string GetCommunicationMethod(PowerDisplay.Core.Models.Monitor monitor)
{
if (!string.IsNullOrEmpty(monitor.CommunicationMethod))
{
return monitor.CommunicationMethod;
}
switch (monitor.Type)
{
case PowerDisplay.Core.Models.MonitorType.External:
return "DDC/CI";
case PowerDisplay.Core.Models.MonitorType.Internal:
return "WMI";
default:
return "Unknown";
}
}
private string GetMonitorType(PowerDisplay.Core.Models.Monitor monitor)
{
switch (monitor.Type)
{
case PowerDisplay.Core.Models.MonitorType.External:
return "External Monitor";
case PowerDisplay.Core.Models.MonitorType.Internal:
return "Internal Monitor";
default:
return "Unknown";
}
}
/// <summary>
/// Periodically save monitor information to settings file
/// </summary>
public async Task SaveMonitorInfoPeriodicallyAsync(IReadOnlyList<PowerDisplay.Core.Models.Monitor> monitors, CancellationToken cancellationToken)
{
try
{
// Wait for a while to ensure the application is fully initialized
await Task.Delay(2000, cancellationToken);
// First update to settings file
await SaveMonitorInfoAsync(monitors);
// Periodically update settings file (every 60 seconds to reduce file conflicts)
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(60000, cancellationToken); // 60 seconds
await SaveMonitorInfoAsync(monitors);
Logger.LogInfo("Periodic monitor info update completed");
}
}
catch (OperationCanceledException)
{
// Normal cancellation
}
catch (Exception ex)
{
Logger.LogError($"Error in periodic monitor info update: {ex.Message}");
}
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
// Stop the periodic timer
_periodicSaveTimer?.Dispose();
// Flush any remaining changes
if (_hasPendingChanges)
{
CheckAndSavePendingChanges(null);
}
_fileAccessSemaphore?.Dispose();
}
}
}
}

View File

@@ -0,0 +1,206 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using Microsoft.UI.Xaml;
using Microsoft.PowerToys.Settings.UI.Library;
using ManagedCommon;
namespace PowerDisplay.Helpers
{
/// <summary>
/// 管理应用程序主题设置
/// </summary>
public static class ThemeManager
{
private const string ThemeSettingKey = "AppTheme";
private static readonly string SettingsFilePath;
private static readonly ISettingsUtils _settingsUtils = new SettingsUtils();
static ThemeManager()
{
// 使用本地AppData文件夹存储设置
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var appFolder = Path.Combine(localAppData, "PowerDisplay");
// 确保文件夹存在
if (!Directory.Exists(appFolder))
{
Directory.CreateDirectory(appFolder);
}
SettingsFilePath = Path.Combine(appFolder, "theme.settings");
}
/// <summary>
/// 获取保存的主题设置
/// </summary>
public static ElementTheme GetSavedTheme()
{
try
{
if (File.Exists(SettingsFilePath))
{
var savedTheme = File.ReadAllText(SettingsFilePath);
return savedTheme switch
{
"Light" => ElementTheme.Light,
"Dark" => ElementTheme.Dark,
_ => ElementTheme.Default
};
}
}
catch
{
// 如果读取失败,返回默认值
}
return ElementTheme.Default;
}
/// <summary>
/// 保存主题设置
/// </summary>
public static void SaveTheme(ElementTheme theme)
{
try
{
File.WriteAllText(SettingsFilePath, theme.ToString());
}
catch
{
// 忽略保存错误
}
}
/// <summary>
/// 应用主题到窗口
/// </summary>
public static void ApplyTheme(Window window, ElementTheme theme)
{
if (window?.Content is FrameworkElement rootElement)
{
rootElement.RequestedTheme = theme;
SaveTheme(theme);
}
}
/// <summary>
/// 切换主题(深色/浅色)
/// </summary>
public static ElementTheme ToggleTheme(Window window)
{
var currentTheme = GetCurrentTheme(window);
var newTheme = currentTheme == ElementTheme.Light ? ElementTheme.Dark : ElementTheme.Light;
ApplyTheme(window, newTheme);
return newTheme;
}
/// <summary>
/// 获取当前窗口的主题
/// </summary>
public static ElementTheme GetCurrentTheme(Window window)
{
if (window?.Content is FrameworkElement rootElement)
{
return rootElement.RequestedTheme switch
{
ElementTheme.Light => ElementTheme.Light,
ElementTheme.Dark => ElementTheme.Dark,
_ => Application.Current.RequestedTheme == ApplicationTheme.Light
? ElementTheme.Light
: ElementTheme.Dark
};
}
return ElementTheme.Default;
}
/// <summary>
/// 判断是否为深色主题
/// </summary>
public static bool IsDarkTheme(Window window)
{
return GetCurrentTheme(window) == ElementTheme.Dark;
}
/// <summary>
/// 从PowerToys设置中获取主题
/// </summary>
public static ElementTheme GetThemeFromPowerToysSettings()
{
try
{
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("PowerDisplay");
return settings.Properties.Theme switch
{
"Light" => ElementTheme.Light,
"Dark" => ElementTheme.Dark,
_ => ElementTheme.Default
};
}
catch
{
return ElementTheme.Default;
}
}
/// <summary>
/// 将主题保存到PowerToys设置
/// </summary>
public static void SaveThemeToPowerToysSettings(ElementTheme theme)
{
try
{
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("PowerDisplay");
settings.Properties.Theme = theme.ToString();
_settingsUtils.SaveSettings(settings.ToJsonString(), "PowerDisplay");
}
catch (Exception ex)
{
// 记录错误但不阻止操作
try
{
Logger.LogError($"Failed to save theme to PowerToys settings: {ex.Message}");
}
catch
{
// 忽略日志错误
}
}
}
/// <summary>
/// 获取保存的主题设置优先从PowerToys设置读取
/// </summary>
public static ElementTheme GetSavedThemeWithPriority()
{
// 首先尝试从PowerToys设置读取
var powerToysTheme = GetThemeFromPowerToysSettings();
if (powerToysTheme != ElementTheme.Default)
{
// 同步到本地设置
SaveTheme(powerToysTheme);
return powerToysTheme;
}
// 如果PowerToys设置没有或失败回退到本地设置
return GetSavedTheme();
}
/// <summary>
/// 应用主题并同步到两个设置系统
/// </summary>
public static void ApplyThemeAndSync(Window window, ElementTheme theme)
{
// 应用到窗口
ApplyTheme(window, theme);
// 同步到PowerToys设置
SaveThemeToPowerToysSettings(theme);
}
}
}

View File

@@ -0,0 +1,571 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Drawing;
using System.Runtime.InteropServices;
using ManagedCommon;
using Microsoft.UI.Xaml;
namespace PowerDisplay.Helpers
{
/// <summary>
/// System tray icon helper class
/// </summary>
public class TrayIconHelper : IDisposable
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct NOTIFYICONDATA
{
public uint CbSize;
public IntPtr HWnd;
public uint UID;
public uint UFlags;
public uint UCallbackMessage;
public IntPtr HIcon;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string SzTip;
public uint DwState;
public uint DwStateMask;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string SzInfo;
public uint UTimeout;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
public string SzInfoTitle;
public uint DwInfoFlags;
}
private const uint NifMessage = 0x00000001;
private const uint NifIcon = 0x00000002;
private const uint NifTip = 0x00000004;
private const uint NifInfo = 0x00000010;
private const uint NimAdd = 0x00000000;
private const uint NimModify = 0x00000001;
private const uint NimDelete = 0x00000002;
private const uint WmUser = 0x0400;
private const uint WmTrayicon = WmUser + 1;
private const uint WmLbuttonup = 0x0202;
private const uint WmRbuttonup = 0x0205;
private const uint WmCommand = 0x0111;
private uint _wmTaskbarCreated; // TaskbarCreated message ID
// Menu item IDs
private const int IdShow = 1001;
private const int IdExit = 1002;
private const int IdRefresh = 1003;
private const int IdSettings = 1004;
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern bool Shell_NotifyIcon(uint dwMessage, ref NOTIFYICONDATA lpData);
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern IntPtr CreateWindowEx(
uint dwExStyle,
string lpClassName,
string lpWindowName,
uint dwStyle,
int x,
int y,
int nWidth,
int nHeight,
IntPtr hWndParent,
IntPtr hMenu,
IntPtr hInstance,
IntPtr lpParam);
[DllImport("user32.dll")]
private static extern bool DestroyWindow(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern IntPtr DefWindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
private static extern IntPtr LoadIcon(IntPtr hInstance, IntPtr lpIconName);
[DllImport("user32.dll")]
private static extern IntPtr CreatePopupMenu();
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool AppendMenu(IntPtr hMenu, uint uFlags, uint uIDNewItem, string lpNewItem);
[DllImport("user32.dll")]
private static extern bool DestroyMenu(IntPtr hMenu);
[DllImport("user32.dll")]
private static extern int TrackPopupMenu(
IntPtr hMenu,
uint uFlags,
int x,
int y,
int nReserved,
IntPtr hWnd,
IntPtr prcRect);
[DllImport("user32.dll")]
private static extern bool GetCursorPos(out POINT lpPoint);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern uint RegisterWindowMessage(string lpString);
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[StructLayout(LayoutKind.Sequential)]
private struct POINT
{
public int X;
public int Y;
}
private delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
private const uint MfString = 0x00000000;
private const uint MfSeparator = 0x00000800;
private const uint TpmLeftalign = 0x0000;
private const uint TpmReturncmd = 0x0100;
private const int SwHide = 0;
private const int SwShow = 5;
private IntPtr _messageWindowHandle;
private NOTIFYICONDATA _notifyIconData;
private bool _isDisposed;
private WndProc _wndProc;
private Window _mainWindow;
private Action? _onShowWindow;
private Action? _onExitApplication;
private Action? _onRefresh;
private Action? _onSettings;
private bool _isWindowVisible = true;
private System.Drawing.Icon? _trayIcon; // Keep icon reference to prevent garbage collection
public TrayIconHelper(Window mainWindow)
{
_mainWindow = mainWindow;
_wndProc = WindowProc;
// Register TaskbarCreated message
_wmTaskbarCreated = RegisterWindowMessage("TaskbarCreated");
Logger.LogInfo($"Registered TaskbarCreated message: {_wmTaskbarCreated}");
if (!CreateMessageWindow())
{
Logger.LogError("Failed to create message window");
return;
}
CreateTrayIcon();
}
/// <summary>
/// Set callback functions
/// </summary>
public void SetCallbacks(Action onShow, Action onExit, Action? onRefresh = null, Action? onSettings = null)
{
_onShowWindow = onShow;
_onExitApplication = onExit;
_onRefresh = onRefresh;
_onSettings = onSettings;
}
/// <summary>
/// Create message window - using system predefined Message window class
/// </summary>
private bool CreateMessageWindow()
{
try
{
Logger.LogDebug("Creating message window using system Message class...");
// Use system predefined "Message" window class, no registration needed
// HWND_MESSAGE (-3) creates pure message window, no hInstance needed
_messageWindowHandle = CreateWindowEx(
0, // dwExStyle
"Message", // lpClassName - system predefined message window class
string.Empty, // lpWindowName
0, // dwStyle
0, 0, 0, 0, // x, y, width, height
new IntPtr(-3), // hWndParent = HWND_MESSAGE (pure message window)
IntPtr.Zero, // hMenu
IntPtr.Zero, // hInstance - not needed
IntPtr.Zero // lpParam
);
if (_messageWindowHandle == IntPtr.Zero)
{
var error = Marshal.GetLastWin32Error();
Logger.LogError($"CreateWindowEx failed with error: {error}");
return false;
}
Logger.LogInfo($"Message window created successfully: {_messageWindowHandle}");
// Set window procedure to handle our messages
SetWindowLongPtr(_messageWindowHandle, -4, Marshal.GetFunctionPointerForDelegate(_wndProc));
return true;
}
catch (Exception ex)
{
Logger.LogError($"CreateMessageWindow exception: {ex.Message}");
return false;
}
}
/// <summary>
/// Create tray icon
/// </summary>
private void CreateTrayIcon()
{
if (_messageWindowHandle == IntPtr.Zero)
{
Logger.LogError("Cannot create tray icon: invalid message window handle");
return;
}
// First try to delete any existing old icon (if any)
var tempData = new NOTIFYICONDATA
{
CbSize = (uint)Marshal.SizeOf(typeof(NOTIFYICONDATA)),
HWnd = _messageWindowHandle,
UID = 1
};
Shell_NotifyIcon(NimDelete, ref tempData);
// Get icon handle
var iconHandle = GetDefaultIcon();
if (iconHandle == IntPtr.Zero)
{
Logger.LogError("Cannot create tray icon: invalid icon handle");
return;
}
_notifyIconData = new NOTIFYICONDATA
{
CbSize = (uint)Marshal.SizeOf(typeof(NOTIFYICONDATA)),
HWnd = _messageWindowHandle,
UID = 1,
UFlags = NifMessage | NifIcon | NifTip,
UCallbackMessage = WmTrayicon,
HIcon = iconHandle,
SzTip = "Power Display",
};
// Retry mechanism: try up to 3 times to create tray icon
const int maxRetries = 3;
const int retryDelayMs = 500;
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
Logger.LogDebug($"Creating tray icon (attempt {attempt}/{maxRetries})...");
bool result = Shell_NotifyIcon(NimAdd, ref _notifyIconData);
if (result)
{
Logger.LogInfo($"Tray icon created successfully on attempt {attempt}");
return;
}
var lastError = Marshal.GetLastWin32Error();
Logger.LogWarning($"Failed to create tray icon on attempt {attempt}. Error: {lastError}");
// Analyze specific error and provide suggestions
switch (lastError)
{
case 0: // ERROR_SUCCESS - may be false success
Logger.LogWarning("Shell_NotifyIcon returned false but GetLastWin32Error is 0");
break;
case 1400: // ERROR_INVALID_WINDOW_HANDLE
Logger.LogWarning("Invalid window handle - message window may not be properly created");
break;
case 1418: // ERROR_THREAD_1_INACTIVE
Logger.LogWarning("Thread inactive - may need to wait for Explorer to be ready");
break;
case 1414: // ERROR_INVALID_ICON_HANDLE
Logger.LogWarning("Invalid icon handle - icon may have been garbage collected");
break;
default:
Logger.LogWarning($"Unexpected error code: {lastError}");
break;
}
// If not the last attempt, wait and retry
if (attempt < maxRetries)
{
Logger.LogDebug($"Retrying in {retryDelayMs}ms...");
System.Threading.Thread.Sleep(retryDelayMs);
// Re-get icon handle to prevent handle invalidation
iconHandle = GetDefaultIcon();
_notifyIconData.HIcon = iconHandle;
}
}
Logger.LogError($"Failed to create tray icon after {maxRetries} attempts");
}
/// <summary>
/// Get default icon
/// </summary>
private IntPtr GetDefaultIcon()
{
try
{
// First release previous icon
_trayIcon?.Dispose();
_trayIcon = null;
// Try to load icon from Assets folder in exe directory
var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName;
if (!string.IsNullOrEmpty(exePath))
{
var exeDir = System.IO.Path.GetDirectoryName(exePath);
if (!string.IsNullOrEmpty(exeDir))
{
var iconPath = System.IO.Path.Combine(exeDir, "Assets", "PowerDisplay.ico");
Logger.LogDebug($"Attempting to load icon from: {iconPath}");
if (System.IO.File.Exists(iconPath))
{
// Create icon and save as class member to prevent garbage collection
_trayIcon = new System.Drawing.Icon(iconPath);
Logger.LogInfo($"Successfully loaded custom icon from {iconPath}");
return _trayIcon.Handle;
}
else
{
Logger.LogWarning($"Icon file not found at: {iconPath}");
}
}
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to load PowerDisplay icon: {ex.Message}");
_trayIcon?.Dispose();
_trayIcon = null;
}
// If loading fails, use system default icon
var systemIconHandle = LoadIcon(IntPtr.Zero, new IntPtr(32512)); // IDI_APPLICATION
Logger.LogInfo($"Using system default icon: {systemIconHandle}");
return systemIconHandle;
}
/// <summary>
/// Window message processing
/// </summary>
private IntPtr WindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
if (msg == _wmTaskbarCreated)
{
// Explorer restarted, need to recreate tray icon
Logger.LogInfo("TaskbarCreated message received - recreating tray icon");
CreateTrayIcon();
return IntPtr.Zero;
}
switch (msg)
{
case WmTrayicon:
HandleTrayIconMessage(lParam);
break;
case WmCommand:
HandleMenuCommand(wParam);
break;
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}
/// <summary>
/// Handle tray icon messages
/// </summary>
private void HandleTrayIconMessage(IntPtr lParam)
{
switch ((uint)lParam)
{
case WmLbuttonup:
// Left click - show/hide window
ToggleWindowVisibility();
break;
case WmRbuttonup:
// Right click - show menu
ShowContextMenu();
break;
}
}
/// <summary>
/// Toggle window visibility state
/// </summary>
private void ToggleWindowVisibility()
{
_isWindowVisible = !_isWindowVisible;
if (_isWindowVisible)
{
_onShowWindow?.Invoke();
}
else
{
// Hide window logic will be implemented in MainWindow
if (_mainWindow != null)
{
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(_mainWindow);
ShowWindow(hWnd, SwHide);
}
}
}
/// <summary>
/// Show right-click menu
/// </summary>
private void ShowContextMenu()
{
var hMenu = CreatePopupMenu();
AppendMenu(hMenu, MfString, IdShow, _isWindowVisible ? "Hide Window" : "Show Window");
if (_onRefresh != null)
{
AppendMenu(hMenu, MfString, IdRefresh, "Refresh Monitors");
}
if (_onSettings != null)
{
AppendMenu(hMenu, MfString, IdSettings, "Settings");
}
AppendMenu(hMenu, MfSeparator, 0, string.Empty);
AppendMenu(hMenu, MfString, IdExit, "Exit");
GetCursorPos(out POINT pt);
SetForegroundWindow(_messageWindowHandle);
var cmd = TrackPopupMenu(hMenu, TpmLeftalign | TpmReturncmd, pt.X, pt.Y, 0, _messageWindowHandle, IntPtr.Zero);
if (cmd != 0)
{
HandleMenuCommand(new IntPtr(cmd));
}
DestroyMenu(hMenu);
}
/// <summary>
/// Handle menu commands
/// </summary>
private void HandleMenuCommand(IntPtr commandId)
{
switch (commandId.ToInt32())
{
case IdShow:
ToggleWindowVisibility();
break;
case IdRefresh:
_onRefresh?.Invoke();
break;
case IdSettings:
_onSettings?.Invoke();
break;
case IdExit:
_onExitApplication?.Invoke();
break;
}
}
/// <summary>
/// Show balloon tip
/// </summary>
public void ShowBalloonTip(string title, string text, uint timeout = 3000)
{
_notifyIconData.UFlags |= NifInfo;
_notifyIconData.SzInfoTitle = title;
_notifyIconData.SzInfo = text;
_notifyIconData.UTimeout = timeout;
_notifyIconData.DwInfoFlags = 1; // NIIF_INFO
Shell_NotifyIcon(NimModify, ref _notifyIconData);
}
/// <summary>
/// Update tray icon tooltip text
/// </summary>
public void UpdateTooltip(string tooltip)
{
_notifyIconData.SzTip = tooltip;
Shell_NotifyIcon(NimModify, ref _notifyIconData);
}
/// <summary>
/// Recreate tray icon (for failure recovery)
/// </summary>
public void RecreateTrayIcon()
{
Logger.LogInfo("Manually recreating tray icon...");
CreateTrayIcon();
}
public void Dispose()
{
if (!_isDisposed)
{
Logger.LogDebug("Disposing TrayIconHelper...");
// Remove tray icon
try
{
Shell_NotifyIcon(NimDelete, ref _notifyIconData);
Logger.LogInfo("Tray icon removed successfully");
}
catch (Exception ex)
{
Logger.LogError($"Error removing tray icon: {ex.Message}");
}
// Release icon resources
try
{
_trayIcon?.Dispose();
_trayIcon = null;
Logger.LogInfo("Icon resources disposed successfully");
}
catch (Exception ex)
{
Logger.LogError($"Error disposing icon: {ex.Message}");
}
// Destroy message window
try
{
if (_messageWindowHandle != IntPtr.Zero)
{
DestroyWindow(_messageWindowHandle);
_messageWindowHandle = IntPtr.Zero;
Logger.LogInfo("Message window destroyed successfully");
}
}
catch (Exception ex)
{
Logger.LogError($"Error destroying message window: {ex.Message}");
}
_isDisposed = true;
GC.SuppressFinalize(this);
Logger.LogDebug("TrayIconHelper disposed completely");
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,610 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using ManagedCommon;
using static PowerDisplay.Native.NativeConstants;
using static PowerDisplay.Native.NativeDelegates;
// 类型别名,兼容 Windows API 命名约定
using PHYSICAL_MONITOR = PowerDisplay.Native.PhysicalMonitor;
using RECT = PowerDisplay.Native.Rect;
using MONITORINFOEX = PowerDisplay.Native.MonitorInfoEx;
using DISPLAY_DEVICE = PowerDisplay.Native.DisplayDevice;
using LUID = PowerDisplay.Native.Luid;
using DISPLAYCONFIG_TARGET_DEVICE_NAME = PowerDisplay.Native.DISPLAYCONFIG_TARGET_DEVICE_NAME;
using DISPLAYCONFIG_DEVICE_INFO_HEADER = PowerDisplay.Native.DISPLAYCONFIG_DEVICE_INFO_HEADER;
using DISPLAYCONFIG_PATH_INFO = PowerDisplay.Native.DISPLAYCONFIG_PATH_INFO;
using DISPLAYCONFIG_MODE_INFO = PowerDisplay.Native.DISPLAYCONFIG_MODE_INFO;
namespace PowerDisplay.Native.DDC
{
/// <summary>
/// DDC/CI 原生 API 封装
/// </summary>
public static class DdcCiNative
{
// DLL Imports
private const string Dxva2Dll = "Dxva2.dll";
private const string User32Dll = "User32.dll";
// Physical Monitor API
/// <summary>
/// 从 HMONITOR 获取物理显示器数量
/// </summary>
[DllImport(Dxva2Dll, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetNumberOfPhysicalMonitorsFromHMONITOR(
IntPtr hMonitor,
ref uint pdwNumberOfPhysicalMonitors);
/// <summary>
/// 从 HMONITOR 获取物理显示器数组
/// </summary>
[DllImport(Dxva2Dll, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetPhysicalMonitorsFromHMONITOR(
IntPtr hMonitor,
uint dwPhysicalMonitorArraySize,
[Out] PHYSICAL_MONITOR[] pPhysicalMonitorArray);
/// <summary>
/// 销毁物理显示器句柄
/// </summary>
[DllImport(Dxva2Dll, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DestroyPhysicalMonitor(IntPtr hPhysicalMonitor);
/// <summary>
/// 销毁物理显示器数组
/// </summary>
[DllImport(Dxva2Dll, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DestroyPhysicalMonitors(
uint dwPhysicalMonitorArraySize,
PHYSICAL_MONITOR[] pPhysicalMonitorArray);
/// <summary>
/// 获取 VCP 功能和回复
/// </summary>
[DllImport(Dxva2Dll, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetVCPFeatureAndVCPFeatureReply(
IntPtr hPhysicalMonitor,
byte bVCPCode,
IntPtr pvct,
out uint pdwCurrentValue,
out uint pdwMaximumValue);
/// <summary>
/// 设置 VCP 功能
/// </summary>
[DllImport(Dxva2Dll, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetVCPFeature(
IntPtr hPhysicalMonitor,
byte bVCPCode,
uint dwNewValue);
/// <summary>
/// 保存当前设置
/// </summary>
[DllImport(Dxva2Dll, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SaveCurrentSettings(IntPtr hPhysicalMonitor);
/// <summary>
/// 获取功能字符串长度
/// </summary>
[DllImport(Dxva2Dll, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetCapabilitiesStringLength(
IntPtr hPhysicalMonitor,
out uint pdwCapabilitiesStringLengthInCharacters);
/// <summary>
/// 功能请求和功能回复
/// </summary>
[DllImport(Dxva2Dll, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CapabilitiesRequestAndCapabilitiesReply(
IntPtr hPhysicalMonitor,
[Out] IntPtr pszASCIICapabilitiesString,
uint dwCapabilitiesStringLengthInCharacters);
/// <summary>
/// 获取显示器亮度
/// </summary>
[DllImport(Dxva2Dll, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetMonitorBrightness(
IntPtr hPhysicalMonitor,
out uint pdwMinimumBrightness,
out uint pdwCurrentBrightness,
out uint pdwMaximumBrightness);
/// <summary>
/// 设置显示器亮度
/// </summary>
[DllImport(Dxva2Dll, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetMonitorBrightness(
IntPtr hPhysicalMonitor,
uint dwNewBrightness);
/// <summary>
/// 获取显示器对比度
/// </summary>
[DllImport(Dxva2Dll, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetMonitorContrast(
IntPtr hPhysicalMonitor,
out uint pdwMinimumContrast,
out uint pdwCurrentContrast,
out uint pdwMaximumContrast);
/// <summary>
/// 设置显示器对比度
/// </summary>
[DllImport(Dxva2Dll, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetMonitorContrast(
IntPtr hPhysicalMonitor,
uint dwNewContrast);
/// <summary>
/// 获取显示器功能
/// </summary>
[DllImport(Dxva2Dll, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetMonitorCapabilities(
IntPtr hPhysicalMonitor,
out uint pdwMonitorCapabilities,
out uint pdwSupportedColorTemperatures);
// Display Configuration API
/// <summary>
/// 获取显示配置缓冲区大小
/// </summary>
[DllImport(User32Dll, SetLastError = true)]
private static extern int GetDisplayConfigBufferSizes(
uint flags,
out uint numPathArrayElements,
out uint numModeInfoArrayElements);
/// <summary>
/// 查询显示配置
/// </summary>
[DllImport(User32Dll, SetLastError = true)]
private static extern int QueryDisplayConfig(
uint flags,
ref uint numPathArrayElements,
[Out] DISPLAYCONFIG_PATH_INFO[] pathArray,
ref uint numModeInfoArrayElements,
[Out] DISPLAYCONFIG_MODE_INFO[] modeInfoArray,
IntPtr currentTopologyId);
/// <summary>
/// 获取显示配置设备信息
/// </summary>
[DllImport(User32Dll, SetLastError = true)]
private static extern int DisplayConfigGetDeviceInfo(
ref DISPLAYCONFIG_TARGET_DEVICE_NAME deviceName);
// Display Configuration 常量
public const uint QdcAllPaths = 0x00000001;
public const uint QdcOnlyActivePaths = 0x00000002;
public const uint DisplayconfigDeviceInfoGetTargetName = 2;
/// <summary>
/// 枚举显示监视器
/// </summary>
[DllImport(User32Dll, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool EnumDisplayMonitors(
IntPtr hdc,
IntPtr lprcClip,
MonitorEnumProc lpfnEnum,
IntPtr dwData);
/// <summary>
/// 获取显示器信息
/// </summary>
[DllImport(User32Dll, SetLastError = true, CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetMonitorInfo(
IntPtr hMonitor,
ref MONITORINFOEX lpmi);
/// <summary>
/// 枚举显示设备
/// </summary>
[DllImport(User32Dll, SetLastError = true, CharSet = CharSet.Ansi)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumDisplayDevices(
string? lpDevice,
uint iDevNum,
ref DISPLAY_DEVICE lpDisplayDevice,
uint dwFlags);
/// <summary>
/// 从窗口获取显示器句柄
/// </summary>
[DllImport(User32Dll, SetLastError = true)]
public static extern IntPtr MonitorFromWindow(
IntPtr hwnd,
uint dwFlags);
/// <summary>
/// 从点获取显示器句柄
/// </summary>
[DllImport(User32Dll, SetLastError = true)]
public static extern IntPtr MonitorFromPoint(
POINT pt,
uint dwFlags);
/// <summary>
/// 获取最后错误
/// </summary>
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint GetLastError();
// Helper Methods
/// <summary>
/// 获取 VCP 功能值的安全包装
/// </summary>
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
/// <param name="vcpCode">VCP 代码</param>
/// <param name="currentValue">当前值</param>
/// <param name="maxValue">最大值</param>
/// <returns>是否成功</returns>
public static bool TryGetVCPFeature(IntPtr hPhysicalMonitor, byte vcpCode, out uint currentValue, out uint maxValue)
{
currentValue = 0;
maxValue = 0;
if (hPhysicalMonitor == IntPtr.Zero)
{
return false;
}
try
{
return GetVCPFeatureAndVCPFeatureReply(hPhysicalMonitor, vcpCode, IntPtr.Zero, out currentValue, out maxValue);
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// 设置 VCP 功能值的安全包装
/// </summary>
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
/// <param name="vcpCode">VCP 代码</param>
/// <param name="value">新值</param>
/// <returns>是否成功</returns>
public static bool TrySetVCPFeature(IntPtr hPhysicalMonitor, byte vcpCode, uint value)
{
if (hPhysicalMonitor == IntPtr.Zero)
{
return false;
}
try
{
return SetVCPFeature(hPhysicalMonitor, vcpCode, value);
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// 获取高级亮度信息的安全包装
/// </summary>
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
/// <param name="minBrightness">最小亮度</param>
/// <param name="currentBrightness">当前亮度</param>
/// <param name="maxBrightness">最大亮度</param>
/// <returns>是否成功</returns>
public static bool TryGetMonitorBrightness(IntPtr hPhysicalMonitor, out uint minBrightness, out uint currentBrightness, out uint maxBrightness)
{
minBrightness = 0;
currentBrightness = 0;
maxBrightness = 0;
if (hPhysicalMonitor == IntPtr.Zero)
{
return false;
}
try
{
return GetMonitorBrightness(hPhysicalMonitor, out minBrightness, out currentBrightness, out maxBrightness);
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// 设置高级亮度的安全包装
/// </summary>
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
/// <param name="brightness">亮度值</param>
/// <returns>是否成功</returns>
public static bool TrySetMonitorBrightness(IntPtr hPhysicalMonitor, uint brightness)
{
if (hPhysicalMonitor == IntPtr.Zero)
{
return false;
}
try
{
return SetMonitorBrightness(hPhysicalMonitor, brightness);
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// 检查 DDC/CI 连接的有效性
/// </summary>
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
/// <returns>是否连接有效</returns>
public static bool ValidateDdcCiConnection(IntPtr hPhysicalMonitor)
{
if (hPhysicalMonitor == IntPtr.Zero)
{
return false;
}
// 尝试读取基本的 VCP 代码来验证连接
var testCodes = new byte[] { NativeConstants.VcpCodeBrightness, NativeConstants.VcpCodeNewControlValue, NativeConstants.VcpCodeVcpVersion };
foreach (var code in testCodes)
{
if (TryGetVCPFeature(hPhysicalMonitor, code, out _, out _))
{
return true;
}
}
return false;
}
/// <summary>
/// 获取显示器友好名称
/// </summary>
/// <param name="adapterId">适配器 ID</param>
/// <param name="targetId">目标 ID</param>
/// <returns>显示器友好名称,如果获取失败返回 null</returns>
public static string? GetMonitorFriendlyName(LUID adapterId, uint targetId)
{
try
{
var deviceName = new DISPLAYCONFIG_TARGET_DEVICE_NAME
{
Header = new DISPLAYCONFIG_DEVICE_INFO_HEADER
{
Type = DisplayconfigDeviceInfoGetTargetName,
Size = (uint)Marshal.SizeOf<DISPLAYCONFIG_TARGET_DEVICE_NAME>(),
AdapterId = adapterId,
Id = targetId,
},
};
var result = DisplayConfigGetDeviceInfo(ref deviceName);
if (result == 0) // ERROR_SUCCESS
{
return deviceName.MonitorFriendlyDeviceName;
}
}
catch
{
// 忽略错误
}
return null;
}
/// <summary>
/// 通过枚举显示配置获取所有显示器友好名称
/// </summary>
/// <returns>设备路径到友好名称的映射</returns>
public static Dictionary<string, string> GetAllMonitorFriendlyNames()
{
var friendlyNames = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
try
{
// 获取缓冲区大小
var result = GetDisplayConfigBufferSizes(QdcOnlyActivePaths, out uint pathCount, out uint modeCount);
if (result != 0) // ERROR_SUCCESS
{
return friendlyNames;
}
// 分配缓冲区
var paths = new DISPLAYCONFIG_PATH_INFO[pathCount];
var modes = new DISPLAYCONFIG_MODE_INFO[modeCount];
// 查询显示配置
result = QueryDisplayConfig(QdcOnlyActivePaths, ref pathCount, paths, ref modeCount, modes, IntPtr.Zero);
if (result != 0)
{
return friendlyNames;
}
// 获取每个路径的友好名称
for (int i = 0; i < pathCount; i++)
{
var path = paths[i];
var friendlyName = GetMonitorFriendlyName(path.TargetInfo.AdapterId, path.TargetInfo.Id);
if (!string.IsNullOrEmpty(friendlyName))
{
// 使用适配器和目标 ID 作为键
var key = $"{path.TargetInfo.AdapterId}_{path.TargetInfo.Id}";
friendlyNames[key] = friendlyName;
}
}
}
catch
{
// 忽略错误
}
return friendlyNames;
}
/// <summary>
/// 获取显示器的EDID硬件ID信息
/// </summary>
/// <param name="adapterId">适配器ID</param>
/// <param name="targetId">目标ID</param>
/// <returns>硬件ID字符串格式为制造商代码+产品代码</returns>
public static string? GetMonitorHardwareId(LUID adapterId, uint targetId)
{
try
{
var deviceName = new DISPLAYCONFIG_TARGET_DEVICE_NAME
{
Header = new DISPLAYCONFIG_DEVICE_INFO_HEADER
{
Type = DisplayconfigDeviceInfoGetTargetName,
Size = (uint)Marshal.SizeOf<DISPLAYCONFIG_TARGET_DEVICE_NAME>(),
AdapterId = adapterId,
Id = targetId,
},
};
var result = DisplayConfigGetDeviceInfo(ref deviceName);
if (result == 0) // ERROR_SUCCESS
{
// 将制造商ID转换为3字符字符串
var manufacturerId = deviceName.EdidManufactureId;
var manufactureCode = ConvertManufactureIdToString(manufacturerId);
// 将产品ID转换为4位十六进制字符串
var productCode = deviceName.EdidProductCodeId.ToString("X4");
var hardwareId = $"{manufactureCode}{productCode}";
Logger.LogDebug($"GetMonitorHardwareId - ManufacturerId: 0x{manufacturerId:X4}, Code: '{manufactureCode}', ProductCode: '{productCode}', Result: '{hardwareId}'");
return hardwareId;
}
else
{
Logger.LogError($"GetMonitorHardwareId - DisplayConfigGetDeviceInfo failed with result: {result}");
}
}
catch
{
// 忽略错误
}
return null;
}
/// <summary>
/// 将制造商ID转换为3字符制造商代码
/// </summary>
/// <param name="manufacturerId">制造商ID</param>
/// <returns>3字符制造商代码</returns>
private static string ConvertManufactureIdToString(ushort manufacturerId)
{
// EDID制造商ID需要先进行字节序交换
manufacturerId = (ushort)(((manufacturerId & 0xff00) >> 8) | ((manufacturerId & 0x00ff) << 8));
// 提取3个5位字符每个字符是A-Z其中A=1, B=2, ..., Z=26
var char1 = (char)('A' - 1 + ((manufacturerId >> 0) & 0x1f));
var char2 = (char)('A' - 1 + ((manufacturerId >> 5) & 0x1f));
var char3 = (char)('A' - 1 + ((manufacturerId >> 10) & 0x1f));
// 按正确顺序组合字符
return $"{char3}{char2}{char1}";
}
/// <summary>
/// 获取所有显示器的完整信息包括友好名称和硬件ID
/// </summary>
/// <returns>包含显示器信息的字典</returns>
public static Dictionary<string, MonitorDisplayInfo> GetAllMonitorDisplayInfo()
{
var monitorInfo = new Dictionary<string, MonitorDisplayInfo>(StringComparer.OrdinalIgnoreCase);
try
{
// 获取缓冲区大小
var result = GetDisplayConfigBufferSizes(QdcOnlyActivePaths, out uint pathCount, out uint modeCount);
if (result != 0) // ERROR_SUCCESS
{
return monitorInfo;
}
// 分配缓冲区
var paths = new DISPLAYCONFIG_PATH_INFO[pathCount];
var modes = new DISPLAYCONFIG_MODE_INFO[modeCount];
// 查询显示配置
result = QueryDisplayConfig(QdcOnlyActivePaths, ref pathCount, paths, ref modeCount, modes, IntPtr.Zero);
if (result != 0)
{
return monitorInfo;
}
// 获取每个路径的信息
for (int i = 0; i < pathCount; i++)
{
var path = paths[i];
var friendlyName = GetMonitorFriendlyName(path.TargetInfo.AdapterId, path.TargetInfo.Id);
var hardwareId = GetMonitorHardwareId(path.TargetInfo.AdapterId, path.TargetInfo.Id);
if (!string.IsNullOrEmpty(friendlyName) || !string.IsNullOrEmpty(hardwareId))
{
var key = $"{path.TargetInfo.AdapterId}_{path.TargetInfo.Id}";
monitorInfo[key] = new MonitorDisplayInfo
{
FriendlyName = friendlyName ?? string.Empty,
HardwareId = hardwareId ?? string.Empty,
AdapterId = path.TargetInfo.AdapterId,
TargetId = path.TargetInfo.Id
};
}
}
}
catch
{
// 忽略错误
}
return monitorInfo;
}
}
/// <summary>
/// 显示器显示信息结构
/// </summary>
public struct MonitorDisplayInfo
{
public string FriendlyName { get; set; }
public string HardwareId { get; set; }
public LUID AdapterId { get; set; }
public uint TargetId { get; set; }
}
}

View File

@@ -0,0 +1,304 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace PowerDisplay.Native
{
/// <summary>
/// Windows API constant definitions
/// </summary>
public static class NativeConstants
{
/// <summary>
/// VCP code: Brightness (primary usage)
/// </summary>
public const byte VcpCodeBrightness = 0x10;
/// <summary>
/// VCP code: Contrast
/// </summary>
public const byte VcpCodeContrast = 0x12;
/// <summary>
/// VCP code: Backlight control (alternative brightness)
/// </summary>
public const byte VcpCodeBacklightControl = 0x13;
/// <summary>
/// VCP code: White backlight level
/// </summary>
public const byte VcpCodeBacklightLevelWhite = 0x6B;
/// <summary>
/// VCP code: Audio speaker volume
/// </summary>
public const byte VcpCodeVolume = 0x62;
/// <summary>
/// VCP code: Audio mute
/// </summary>
public const byte VcpCodeMute = 0x8D;
/// <summary>
/// VCP code: Color temperature request (主要色温控制)
/// </summary>
public const byte VcpCodeColorTemperature = 0x0C;
/// <summary>
/// VCP code: Color temperature increment (色温增量调节)
/// </summary>
public const byte VcpCodeColorTemperatureIncrement = 0x0B;
/// <summary>
/// VCP code: Gamma correction (Gamma调节)
/// </summary>
public const byte VcpCodeGamma = 0x72;
/// <summary>
/// VCP code: Select color preset (颜色预设选择)
/// </summary>
public const byte VcpCodeSelectColorPreset = 0x14;
/// <summary>
/// VCP code: VCP version
/// </summary>
public const byte VcpCodeVcpVersion = 0xDF;
/// <summary>
/// VCP code: New control value
/// </summary>
public const byte VcpCodeNewControlValue = 0x02;
/// <summary>
/// Display device attached to desktop
/// </summary>
public const uint DisplayDeviceAttachedToDesktop = 0x00000001;
/// <summary>
/// Multi-monitor primary display
/// </summary>
public const uint DisplayDeviceMultiDriver = 0x00000002;
/// <summary>
/// Primary device
/// </summary>
public const uint DisplayDevicePrimaryDevice = 0x00000004;
/// <summary>
/// Mirroring driver
/// </summary>
public const uint DisplayDeviceMirroringDriver = 0x00000008;
/// <summary>
/// VGA compatible
/// </summary>
public const uint DisplayDeviceVgaCompatible = 0x00000010;
/// <summary>
/// Removable device
/// </summary>
public const uint DisplayDeviceRemovable = 0x00000020;
/// <summary>
/// Get device interface name
/// </summary>
public const uint EddGetDeviceInterfaceName = 0x00000001;
/// <summary>
/// Primary monitor
/// </summary>
public const uint MonitorinfoFPrimary = 0x00000001;
/// <summary>
/// Query display config: only active paths
/// </summary>
public const uint QdcOnlyActivePaths = 0x00000002;
/// <summary>
/// Query display config: all paths
/// </summary>
public const uint QdcAllPaths = 0x00000001;
/// <summary>
/// Set display config: apply
/// </summary>
public const uint SdcApply = 0x00000080;
/// <summary>
/// Set display config: use supplied display config
/// </summary>
public const uint SdcUseSuppliedDisplayConfig = 0x00000020;
/// <summary>
/// Set display config: save to database
/// </summary>
public const uint SdcSaveToDatabase = 0x00000200;
/// <summary>
/// Set display config: topology supplied
/// </summary>
public const uint SdcTopologySupplied = 0x00000010;
/// <summary>
/// Set display config: allow path order changes
/// </summary>
public const uint SdcAllowPathOrderChanges = 0x00002000;
/// <summary>
/// Get target name
/// </summary>
public const uint DisplayconfigDeviceInfoGetTargetName = 1;
/// <summary>
/// Get SDR white level
/// </summary>
public const uint DisplayconfigDeviceInfoGetSdrWhiteLevel = 7;
/// <summary>
/// Get advanced color information
/// </summary>
public const uint DisplayconfigDeviceInfoGetAdvancedColorInfo = 9;
/// <summary>
/// Set SDR white level (custom)
/// </summary>
public const uint DisplayconfigDeviceInfoSetSdrWhiteLevel = 0xFFFFFFEE;
/// <summary>
/// Path active
/// </summary>
public const uint DisplayconfigPathActive = 0x00000001;
/// <summary>
/// Path mode index invalid
/// </summary>
public const uint DisplayconfigPathModeIdxInvalid = 0xFFFFFFFF;
/// <summary>
/// COM initialization: multithreaded
/// </summary>
public const uint CoinitMultithreaded = 0x0;
/// <summary>
/// RPC authentication level: connect
/// </summary>
public const uint RpcCAuthnLevelConnect = 2;
/// <summary>
/// RPC impersonation level: impersonate
/// </summary>
public const uint RpcCImpLevelImpersonate = 3;
/// <summary>
/// RPC authentication service: Win NT
/// </summary>
public const uint RpcCAuthnWinnt = 10;
/// <summary>
/// RPC authorization service: none
/// </summary>
public const uint RpcCAuthzNone = 0;
/// <summary>
/// RPC authentication level: call
/// </summary>
public const uint RpcCAuthnLevelCall = 3;
/// <summary>
/// EOAC: none
/// </summary>
public const uint EoacNone = 0;
/// <summary>
/// WMI flag: forward only
/// </summary>
public const long WbemFlagForwardOnly = 0x20;
/// <summary>
/// WMI flag: return immediately
/// </summary>
public const long WbemFlagReturnImmediately = 0x10;
/// <summary>
/// WMI flag: connect use max wait
/// </summary>
public const long WbemFlagConnectUseMaxWait = 0x80;
/// <summary>
/// Success
/// </summary>
public const int ErrorSuccess = 0;
/// <summary>
/// Insufficient buffer
/// </summary>
public const int ErrorInsufficientBuffer = 122;
/// <summary>
/// Invalid parameter
/// </summary>
public const int ErrorInvalidParameter = 87;
/// <summary>
/// Access denied
/// </summary>
public const int ErrorAccessDenied = 5;
/// <summary>
/// General failure
/// </summary>
public const int ErrorGenFailure = 31;
/// <summary>
/// Unsupported VCP code
/// </summary>
public const int ErrorGraphicsDdcciVcpNotSupported = -1071243251;
/// <summary>
/// Infinite wait
/// </summary>
public const uint Infinite = 0xFFFFFFFF;
/// <summary>
/// User message
/// </summary>
public const uint WmUser = 0x0400;
/// <summary>
/// Output technology: HDMI
/// </summary>
public const uint DisplayconfigOutputTechnologyHdmi = 5;
/// <summary>
/// Output technology: DVI
/// </summary>
public const uint DisplayconfigOutputTechnologyDvi = 4;
/// <summary>
/// Output technology: DisplayPort
/// </summary>
public const uint DisplayconfigOutputTechnologyDisplayportExternal = 6;
/// <summary>
/// Output technology: internal
/// </summary>
public const uint DisplayconfigOutputTechnologyInternal = 0x80000000;
/// <summary>
/// HDR minimum SDR white level (nits)
/// </summary>
public const int HdrMinSdrWhiteLevel = 80;
/// <summary>
/// HDR maximum SDR white level (nits)
/// </summary>
public const int HdrMaxSdrWhiteLevel = 480;
/// <summary>
/// SDR white level conversion factor
/// </summary>
public const int SdrWhiteLevelFactor = 80;
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
// 类型别名,兼容 Windows API 命名约定
using RECT = PowerDisplay.Native.Rect;
namespace PowerDisplay.Native;
/// <summary>
/// 委托类型定义
/// </summary>
public static class NativeDelegates
{
/// <summary>
/// 显示器枚举过程委托
/// </summary>
/// <param name="hMonitor">显示器句柄</param>
/// <param name="hdcMonitor">显示器 DC</param>
/// <param name="lprcMonitor">显示器矩形</param>
/// <param name="dwData">用户数据</param>
/// <returns>继续枚举返回 true</returns>
public delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData);
/// <summary>
/// 线程启动例程委托
/// </summary>
/// <param name="lpParameter">线程参数</param>
/// <returns>线程退出代码</returns>
public delegate uint ThreadStartRoutine(IntPtr lpParameter);
}

View File

@@ -0,0 +1,355 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace PowerDisplay.Native
{
/// <summary>
/// Physical monitor structure
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct PhysicalMonitor
{
/// <summary>
/// Physical monitor handle
/// </summary>
public IntPtr HPhysicalMonitor;
/// <summary>
/// Physical monitor description (128 characters)
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string SzPhysicalMonitorDescription;
}
/// <summary>
/// Rectangle structure
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct Rect
{
public int Left;
public int Top;
public int Right;
public int Bottom;
public int Width => Right - Left;
public int Height => Bottom - Top;
public Rect(int left, int top, int right, int bottom)
{
Left = left;
Top = top;
Right = right;
Bottom = bottom;
}
}
/// <summary>
/// Monitor information extended structure
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct MonitorInfoEx
{
/// <summary>
/// Structure size
/// </summary>
public uint CbSize;
/// <summary>
/// Monitor rectangle area
/// </summary>
public Rect RcMonitor;
/// <summary>
/// Work area rectangle
/// </summary>
public Rect RcWork;
/// <summary>
/// Flags
/// </summary>
public uint DwFlags;
/// <summary>
/// Device name (e.g., "\\.\DISPLAY1")
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string SzDevice;
}
/// <summary>
/// Display device structure
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DisplayDevice
{
/// <summary>
/// Structure size
/// </summary>
public uint Cb;
/// <summary>
/// Device name (e.g., "\\.\DISPLAY1\Monitor0")
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string DeviceName;
/// <summary>
/// Device description string
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string DeviceString;
/// <summary>
/// Status flags
/// </summary>
public uint StateFlags;
/// <summary>
/// Device ID
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string DeviceID;
/// <summary>
/// Registry device key
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string DeviceKey;
}
/// <summary>
/// LUID (Locally Unique Identifier) structure
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct Luid
{
public uint LowPart;
public int HighPart;
public override string ToString()
{
return $"{HighPart:X8}:{LowPart:X8}";
}
}
/// <summary>
/// Display configuration path information
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_PATH_INFO
{
public DISPLAYCONFIG_PATH_SOURCE_INFO SourceInfo;
public DISPLAYCONFIG_PATH_TARGET_INFO TargetInfo;
public uint Flags;
}
/// <summary>
/// Display configuration path source information
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_PATH_SOURCE_INFO
{
public Luid AdapterId;
public uint Id;
public uint ModeInfoIdx;
public uint StatusFlags;
}
/// <summary>
/// Display configuration path target information
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_PATH_TARGET_INFO
{
public Luid AdapterId;
public uint Id;
public uint ModeInfoIdx;
public uint OutputTechnology;
public uint Rotation;
public uint Scaling;
public DISPLAYCONFIG_RATIONAL RefreshRate;
public uint ScanLineOrdering;
public bool TargetAvailable;
public uint StatusFlags;
}
/// <summary>
/// Display configuration rational number
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_RATIONAL
{
public uint Numerator;
public uint Denominator;
}
/// <summary>
/// Display configuration mode information
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_MODE_INFO
{
public uint InfoType;
public uint Id;
public Luid AdapterId;
public DISPLAYCONFIG_MODE_INFO_UNION ModeInfo;
}
/// <summary>
/// Display configuration mode information union
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct DISPLAYCONFIG_MODE_INFO_UNION
{
[FieldOffset(0)]
public DISPLAYCONFIG_TARGET_MODE targetMode;
[FieldOffset(0)]
public DISPLAYCONFIG_SOURCE_MODE sourceMode;
}
/// <summary>
/// Display configuration target mode
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_TARGET_MODE
{
public DISPLAYCONFIG_VIDEO_SIGNAL_INFO TargetVideoSignalInfo;
}
/// <summary>
/// Display configuration source mode
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_SOURCE_MODE
{
public uint Width;
public uint Height;
public uint PixelFormat;
public DISPLAYCONFIG_POINT Position;
}
/// <summary>
/// Display configuration point
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_POINT
{
public int X;
public int Y;
}
/// <summary>
/// Display configuration video signal information
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_VIDEO_SIGNAL_INFO
{
public ulong PixelRate;
public DISPLAYCONFIG_RATIONAL HSyncFreq;
public DISPLAYCONFIG_RATIONAL VSyncFreq;
public DISPLAYCONFIG_2DREGION ActiveSize;
public DISPLAYCONFIG_2DREGION TotalSize;
public uint VideoStandard;
public uint ScanLineOrdering;
}
/// <summary>
/// Display configuration 2D region
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_2DREGION
{
public uint Cx;
public uint Cy;
}
/// <summary>
/// Display configuration device information header
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_DEVICE_INFO_HEADER
{
public uint Type;
public uint Size;
public Luid AdapterId;
public uint Id;
}
/// <summary>
/// Display configuration target device name
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DISPLAYCONFIG_TARGET_DEVICE_NAME
{
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
public uint Flags;
public uint OutputTechnology;
public ushort EdidManufactureId;
public ushort EdidProductCodeId;
public uint ConnectorInstance;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
public string MonitorFriendlyDeviceName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string MonitorDevicePath;
}
/// <summary>
/// Display configuration SDR white level
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_SDR_WHITE_LEVEL
{
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
public uint SDRWhiteLevel;
}
/// <summary>
/// Display configuration advanced color information
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO
{
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
public uint AdvancedColorSupported;
public uint AdvancedColorEnabled;
public uint BitsPerColorChannel;
public uint ColorEncoding;
public uint FormatSupport;
}
/// <summary>
/// Custom structure for setting SDR white level
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_SET_SDR_WHITE_LEVEL
{
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
public uint SDRWhiteLevel;
public byte FinalValue;
}
/// <summary>
/// Point structure
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
public POINT(int x, int y)
{
this.X = x;
this.Y = y;
}
}
}

View File

@@ -0,0 +1,365 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Management;
using System.Threading;
using System.Threading.Tasks;
using PowerDisplay.Core.Interfaces;
using PowerDisplay.Core.Models;
using Monitor = PowerDisplay.Core.Models.Monitor;
namespace PowerDisplay.Native.WMI
{
/// <summary>
/// WMI monitor controller for controlling internal laptop displays
/// </summary>
public class WmiController : IMonitorController, IDisposable
{
private const string WmiNamespace = "root\\WMI";
private const string BrightnessQueryClass = "WmiMonitorBrightness";
private const string BrightnessMethodClass = "WmiMonitorBrightnessMethods";
private const string MonitorIdClass = "WmiMonitorID";
private bool _disposed = false;
public string Name => "WMI Monitor Controller";
public MonitorType SupportedType => MonitorType.Internal;
/// <summary>
/// Check if the specified monitor can be controlled
/// </summary>
public async Task<bool> CanControlMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
if (monitor.Type != MonitorType.Internal)
return false;
return await Task.Run(() =>
{
try
{
using var searcher = new ManagementObjectSearcher(WmiNamespace, $"SELECT * FROM {BrightnessQueryClass}");
using var collection = searcher.Get();
return collection.Count > 0;
}
catch (Exception)
{
return false;
}
}, cancellationToken);
}
/// <summary>
/// Get monitor brightness
/// </summary>
public async Task<BrightnessInfo> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
return await Task.Run(() =>
{
try
{
using var searcher = new ManagementObjectSearcher(WmiNamespace,
$"SELECT CurrentBrightness FROM {BrightnessQueryClass}");
using var collection = searcher.Get();
foreach (ManagementObject obj in collection)
{
using (obj)
{
var currentBrightness = Convert.ToInt32(obj["CurrentBrightness"]);
return new BrightnessInfo(currentBrightness, 0, 100);
}
}
}
catch (Exception)
{
// Silent failure
}
return BrightnessInfo.Invalid;
}, cancellationToken);
}
/// <summary>
/// Set monitor brightness
/// </summary>
public async Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default)
{
// Validate brightness range
brightness = Math.Clamp(brightness, 0, 100);
return await Task.Run(() =>
{
try
{
using var searcher = new ManagementObjectSearcher(WmiNamespace,
$"SELECT * FROM {BrightnessMethodClass}");
using var collection = searcher.Get();
foreach (ManagementObject obj in collection)
{
using (obj)
{
// Call WmiSetBrightness method
var result = obj.InvokeMethod("WmiSetBrightness", new object[] { 0, (byte)brightness });
// Check return value (0 indicates success)
var returnValue = result != null ? Convert.ToInt32(result) : -1;
if (returnValue == 0)
{
return MonitorOperationResult.Success();
}
else
{
return MonitorOperationResult.Failure($"WMI method returned error code: {returnValue}", returnValue);
}
}
}
return MonitorOperationResult.Failure("No WMI brightness methods found");
}
catch (UnauthorizedAccessException)
{
return MonitorOperationResult.Failure("Access denied. Administrator privileges may be required.", 5);
}
catch (ManagementException ex)
{
return MonitorOperationResult.Failure($"WMI error: {ex.Message}", (int)ex.ErrorCode);
}
catch (Exception ex)
{
return MonitorOperationResult.Failure($"Unexpected error: {ex.Message}");
}
}, cancellationToken);
}
/// <summary>
/// Discover supported monitors
/// </summary>
public async Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
{
return await Task.Run(() =>
{
var monitors = new List<Monitor>();
try
{
// First check if WMI brightness support is available
using var brightnessSearcher = new ManagementObjectSearcher(WmiNamespace,
$"SELECT * FROM {BrightnessQueryClass}");
using var brightnessCollection = brightnessSearcher.Get();
if (brightnessCollection.Count == 0)
return monitors;
// Get monitor information
using var idSearcher = new ManagementObjectSearcher(WmiNamespace,
$"SELECT * FROM {MonitorIdClass}");
using var idCollection = idSearcher.Get();
var monitorInfos = new Dictionary<string, (string Name, string InstanceName)>();
foreach (ManagementObject obj in idCollection)
{
using (obj)
{
try
{
var instanceName = obj["InstanceName"]?.ToString() ?? "";
var userFriendlyName = GetUserFriendlyName(obj) ?? "Internal Display";
if (!string.IsNullOrEmpty(instanceName))
{
monitorInfos[instanceName] = (userFriendlyName, instanceName);
}
}
catch
{
// Skip problematic entries
}
}
}
// Create monitor objects for each supported brightness instance
foreach (ManagementObject obj in brightnessCollection)
{
using (obj)
{
try
{
var instanceName = obj["InstanceName"]?.ToString() ?? "";
var currentBrightness = Convert.ToInt32(obj["CurrentBrightness"]);
var name = "Internal Display";
if (monitorInfos.TryGetValue(instanceName, out var info))
{
name = info.Name;
}
var monitor = new Monitor
{
Id = $"WMI_{instanceName}",
Name = name,
Type = MonitorType.Internal,
CurrentBrightness = currentBrightness,
MinBrightness = 0,
MaxBrightness = 100,
IsAvailable = true,
InstanceName = instanceName,
Capabilities = MonitorCapabilities.Brightness | MonitorCapabilities.Wmi,
ConnectionType = "Internal",
CommunicationMethod = "WMI",
Manufacturer = "Internal",
SupportsColorTemperature = false // Internal monitors don't support DDC/CI color temperature
};
monitors.Add(monitor);
}
catch
{
// Skip problematic monitors
}
}
}
}
catch (Exception)
{
// Return empty list instead of throwing exception
}
return monitors;
}, cancellationToken);
}
/// <summary>
/// Validate monitor connection status
/// </summary>
public async Task<bool> ValidateConnectionAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
return await Task.Run(() =>
{
try
{
// Try to read current brightness to validate connection
using var searcher = new ManagementObjectSearcher(WmiNamespace,
$"SELECT CurrentBrightness FROM {BrightnessQueryClass} WHERE InstanceName='{monitor.InstanceName}'");
using var collection = searcher.Get();
return collection.Count > 0;
}
catch
{
return false;
}
}, cancellationToken);
}
/// <summary>
/// Get user-friendly name
/// </summary>
private static string? GetUserFriendlyName(ManagementObject monitorObject)
{
try
{
var userFriendlyName = monitorObject["UserFriendlyName"] as ushort[];
if (userFriendlyName != null && userFriendlyName.Length > 0)
{
// Convert UINT16 array to string
var chars = userFriendlyName
.Where(c => c != 0)
.Select(c => (char)c)
.ToArray();
if (chars.Length > 0)
{
return new string(chars).Trim();
}
}
}
catch
{
// Ignore conversion errors
}
return null;
}
/// <summary>
/// Check WMI service availability
/// </summary>
public static bool IsWmiAvailable()
{
try
{
using var searcher = new ManagementObjectSearcher(WmiNamespace,
$"SELECT * FROM {BrightnessQueryClass}");
using var collection = searcher.Get();
return collection.Count > 0;
}
catch
{
return false;
}
}
/// <summary>
/// Check if administrator privileges are required
/// </summary>
public static bool RequiresElevation()
{
try
{
using var searcher = new ManagementObjectSearcher(WmiNamespace,
$"SELECT * FROM {BrightnessMethodClass}");
using var collection = searcher.Get();
foreach (ManagementObject obj in collection)
{
using (obj)
{
// Try to call method to check permissions
try
{
obj.InvokeMethod("WmiSetBrightness", new object[] { 0, 50 });
return false; // If successful, no elevation required
}
catch (UnauthorizedAccessException)
{
return true; // Administrator privileges required
}
catch
{
// Other errors may not be permission issues
return false;
}
}
}
}
catch
{
// Cannot determine, assume privileges required
return true;
}
return false;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed && disposing)
{
// WMI objects are automatically cleaned up, no specific cleanup needed here
_disposed = true;
}
}
}
}

View File

@@ -0,0 +1,161 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace PowerDisplay.Native
{
internal static class WindowHelper
{
// Window Styles
private const int GwlStyle = -16;
private const int WsCaption = 0x00C00000;
private const int WsThickframe = 0x00040000;
private const int WsMinimizebox = 0x00020000;
private const int WsMaximizebox = 0x00010000;
private const int WsSysmenu = 0x00080000;
// Extended Window Styles
private const int GwlExstyle = -20;
private const int WsExDlgmodalframe = 0x00000001;
private const int WsExWindowedge = 0x00000100;
private const int WsExClientedge = 0x00000200;
private const int WsExStaticedge = 0x00020000;
private const int WsExToolwindow = 0x00000080;
// Window Messages
private const int WmNclbuttondown = 0x00A1;
private const int WmSyscommand = 0x0112;
private const int ScMove = 0xF010;
[DllImport("user32.dll", SetLastError = true)]
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll", SetLastError = true)]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
[DllImport("user32.dll")]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
private const uint SwpNosize = 0x0001;
private const uint SwpNomove = 0x0002;
private const uint SwpFramechanged = 0x0020;
private static readonly IntPtr HwndTopmost = new IntPtr(-1);
private static readonly IntPtr HwndNotopmost = new IntPtr(-2);
// ShowWindow commands
private const int SwHide = 0;
private const int SwShow = 5;
private const int SwMinimize = 6;
private const int SwRestore = 9;
/// <summary>
/// 禁用窗口的拖动和缩放功能
/// </summary>
public static void DisableWindowMovingAndResizing(IntPtr hWnd)
{
// 获取当前窗口样式
int style = GetWindowLong(hWnd, GwlStyle);
// 移除可调整大小的边框、标题栏和系统菜单
style &= ~WsThickframe;
style &= ~WsMaximizebox;
style &= ~WsMinimizebox;
style &= ~WsCaption; // 移除整个标题栏
style &= ~WsSysmenu; // 移除系统菜单
// 设置新的窗口样式
_ = SetWindowLong(hWnd, GwlStyle, style);
// 获取扩展样式并移除相关边框
int exStyle = GetWindowLong(hWnd, GwlExstyle);
exStyle &= ~WsExDlgmodalframe;
exStyle &= ~WsExWindowedge;
exStyle &= ~WsExClientedge;
exStyle &= ~WsExStaticedge;
_ = SetWindowLong(hWnd, GwlExstyle, exStyle);
// 刷新窗口框架
SetWindowPos(
hWnd,
IntPtr.Zero,
0,
0,
0,
0,
SwpNomove | SwpNosize | SwpFramechanged);
}
/// <summary>
/// 设置窗口是否置顶
/// </summary>
public static void SetWindowTopmost(IntPtr hWnd, bool topmost)
{
SetWindowPos(
hWnd,
topmost ? HwndTopmost : HwndNotopmost,
0,
0,
0,
0,
SwpNomove | SwpNosize);
}
/// <summary>
/// 显示或隐藏窗口
/// </summary>
public static void ShowWindow(IntPtr hWnd, bool show)
{
ShowWindow(hWnd, show ? SwShow : SwHide);
}
/// <summary>
/// 最小化窗口
/// </summary>
public static void MinimizeWindow(IntPtr hWnd)
{
ShowWindow(hWnd, SwMinimize);
}
/// <summary>
/// 恢复窗口
/// </summary>
public static void RestoreWindow(IntPtr hWnd)
{
ShowWindow(hWnd, SwRestore);
}
/// <summary>
/// 设置窗口不在任务栏显示
/// </summary>
public static void HideFromTaskbar(IntPtr hWnd)
{
// 获取当前扩展样式
int exStyle = GetWindowLong(hWnd, GwlExstyle);
// 添加 WS_EX_TOOLWINDOW 样式,这会让窗口不在任务栏显示
exStyle |= WsExToolwindow;
// 设置新的扩展样式
_ = SetWindowLong(hWnd, GwlExstyle, exStyle);
// 刷新窗口框架
SetWindowPos(
hWnd,
IntPtr.Zero,
0,
0,
0,
0,
SwpNomove | SwpNosize | SwpFramechanged);
}
}
}

View File

@@ -0,0 +1,78 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<RootNamespace>PowerDisplay</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x64;ARM64</Platforms>
<UseWinUI>true</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
<WindowsPackageType>None</WindowsPackageType>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
<AssemblyName>PowerToys.PowerDisplay</AssemblyName>
<ApplicationIcon>Assets\PowerDisplay.ico</ApplicationIcon>
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
<ProjectPriFileName>PowerToys.PowerDisplay.pri</ProjectPriFileName>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<!-- Disable StyleCop for this project -->
<DisableStyleCop>true</DisableStyleCop>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<RunCodeAnalysis>false</RunCodeAnalysis>
</PropertyGroup>
<ItemGroup>
<Page Remove="PowerDisplayXAML\App.xaml" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="PowerDisplayXAML\App.xaml" />
</ItemGroup>
<ItemGroup>
<Folder Include="PowerDisplayXAML\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="System.Management" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" />
<PackageReference Include="CommunityToolkit.WinUI.Animations" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<!--
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
Tools extension to be activated for this project even if the Windows App SDK Nuget
package has not yet been restored.
-->
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<!-- HACK: Common.UI is referenced, even if it is not used, to force dll versions to be the same as in other projects that use it. It's still unclear why this is the case, but this is need for flattening the install directory. -->
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
<!-- Copy Assets folder to output directory -->
<ItemGroup>
<Content Include="Assets\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<!--
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
Explorer "Package and Publish" context menu entry to be enabled for this project even if
the Windows App SDK Nuget package has not yet been restored.
-->
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="utf-8"?>
<Application x:Class="PowerDisplay.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:PowerDisplay.Converters"
xmlns:toolkit="using:CommunityToolkit.WinUI">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- WinUI 3 System Resources -->
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
</ResourceDictionary.MergedDictionaries>
<!-- Converters -->
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<converters:InverseBoolToVisibilityConverter x:Key="InverseBoolToVisibilityConverter"/>
<converters:NoMonitorsVisibilityConverter x:Key="NoMonitorsVisibilityConverter"/>
<!-- Fluent 2 Design Tokens -->
<!-- Corner Radius -->
<CornerRadius x:Key="ControlCornerRadius">4</CornerRadius>
<CornerRadius x:Key="OverlayCornerRadius">8</CornerRadius>
<CornerRadius x:Key="CardCornerRadius">8</CornerRadius>
<!-- Spacing -->
<x:Double x:Key="SmallSpacing">4</x:Double>
<x:Double x:Key="MediumSpacing">8</x:Double>
<x:Double x:Key="LargeSpacing">16</x:Double>
<x:Double x:Key="ExtraLargeSpacing">24</x:Double>
<!-- Elevation (Shadows) -->
<!-- Shadow definitions removed - not supported in WinUI 3 XAML resources -->
<!-- Animations -->
<TransitionCollection x:Key="SettingsCardsAnimations">
<EntranceThemeTransition FromVerticalOffset="50" />
<RepositionThemeTransition IsStaggeringEnabled="False" />
</TransitionCollection>
<!-- Modern Monitor Card Style -->
<Style x:Key="MonitorCardStyle" TargetType="Border">
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="{StaticResource CardCornerRadius}" />
<Setter Property="Padding" Value="16" />
</Style>
<!-- Modern Slider Style -->
<Style x:Key="ModernSliderStyle" TargetType="Slider">
<Setter Property="Foreground" Value="{ThemeResource AccentFillColorDefaultBrush}" />
<Setter Property="Background" Value="{ThemeResource ControlFillColorDefaultBrush}" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="CornerRadius" Value="{StaticResource ControlCornerRadius}" />
</Style>
<!-- Icon Button Style -->
<Style x:Key="IconButtonStyle" TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Width" Value="40" />
<Setter Property="Height" Value="40" />
<Setter Property="CornerRadius" Value="{StaticResource ControlCornerRadius}" />
<Setter Property="Padding" Value="8" />
</Style>
<!-- Monitor Action Button Style -->
<Style x:Key="MonitorActionButtonStyle" TargetType="Button" BasedOn="{StaticResource IconButtonStyle}">
<Setter Property="Background" Value="{ThemeResource SubtleFillColorTransparentBrush}" />
</Style>
<!-- Title Text Style -->
<Style x:Key="MonitorTitleTextStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="16" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
</Style>
<!-- Control Label Style -->
<Style x:Key="ControlLabelStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="12" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
</Style>
<!-- Value Text Style -->
<Style x:Key="ValueTextStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="12" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<!-- Compact Slider Style -->
<Style x:Key="CompactSliderStyle" TargetType="Slider">
<Setter Property="Foreground" Value="{ThemeResource AccentFillColorDefaultBrush}" />
<Setter Property="Background" Value="{ThemeResource ControlFillColorDefaultBrush}" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="CornerRadius" Value="{StaticResource ControlCornerRadius}" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="0,2,0,2" />
</Style>
<!-- Compact Icon Style -->
<Style x:Key="CompactIconStyle" TargetType="FontIcon">
<Setter Property="FontSize" Value="14" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Center" />
</Style>
<!-- Compact Value Text Style -->
<Style x:Key="CompactValueTextStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="12" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<!-- Status Bar Style -->
<Style x:Key="StatusBarStyle" TargetType="Grid">
<Setter Property="Background" Value="{ThemeResource LayerFillColorDefaultBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="0,1,0,0" />
<Setter Property="Padding" Value="8,8" />
</Style>
<!-- Window Background -->
<Style x:Key="WindowBackgroundStyle" TargetType="Grid">
<Setter Property="Background" Value="{ThemeResource ApplicationPageBackgroundThemeBrush}" />
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,323 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using ManagedCommon;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.Windows.AppLifecycle;
using PowerDisplay.Helpers;
using Microsoft.UI.Dispatching;
using PowerToys.Interop;
namespace PowerDisplay
{
/// <summary>
/// PowerDisplay 应用程序主类
/// </summary>
public partial class App : Application
{
private Window? _mainWindow;
private int _powerToysRunnerPid;
private static Mutex? _mutex;
/// <summary>
/// 初始化 PowerDisplay 应用程序
/// </summary>
public App()
{
this.InitializeComponent();
// Initialize Logger
Logger.InitializeLogger("\\PowerDisplay\\Logs");
// Initialize PowerToys telemetry
try
{
PowerToysTelemetry.Log.WriteEvent(new Telemetry.Events.PowerDisplayStartEvent());
}
catch
{
// Telemetry errors should not crash the app
}
// Initialize language settings
string appLanguage = LanguageHelper.LoadLanguage();
if (!string.IsNullOrEmpty(appLanguage))
{
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
}
// 应用保存的主题设置优先从PowerToys设置读取
var savedTheme = ThemeManager.GetSavedThemeWithPriority();
if (savedTheme != ElementTheme.Default)
{
// 转换ElementTheme到ApplicationTheme
this.RequestedTheme = savedTheme == ElementTheme.Light
? ApplicationTheme.Light
: ApplicationTheme.Dark;
}
// 处理未捕获的异常
this.UnhandledException += OnUnhandledException;
}
/// <summary>
/// 处理未捕获的异常
/// </summary>
private void OnUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
// 尝试显示错误信息
ShowStartupError(e.Exception);
// 标记异常已处理,防止应用崩溃
e.Handled = true;
}
/// <summary>
/// 在应用程序启动时调用
/// </summary>
/// <param name="args">启动参数</param>
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
try
{
// 使用 Mutex 确保只有一个 PowerDisplay 实例运行
_mutex = new Mutex(true, "PowerDisplay", out bool isNewInstance);
if (!isNewInstance)
{
// PowerDisplay 已经在运行,退出当前实例
Logger.LogInfo("PowerDisplay is already running. Exiting duplicate instance.");
Environment.Exit(0);
return;
}
// 确保在应用退出时释放 Mutex
AppDomain.CurrentDomain.ProcessExit += (_, _) => _mutex?.ReleaseMutex();
// 解析命令行参数
var cmdArgs = Environment.GetCommandLineArgs();
if (cmdArgs?.Length > 1)
{
// 支持两种格式直接PID或者 --pid PID
int pidValue = -1;
// 检查是否是 --pid 格式
for (int i = 1; i < cmdArgs.Length - 1; i++)
{
if (cmdArgs[i] == "--pid" && int.TryParse(cmdArgs[i + 1], out pidValue))
{
break;
}
}
// 如果不是 --pid 格式,尝试解析最后一个参数(兼容旧格式)
if (pidValue == -1 && cmdArgs.Length > 1)
{
int.TryParse(cmdArgs[cmdArgs.Length - 1], out pidValue);
}
if (pidValue > 0)
{
_powerToysRunnerPid = pidValue;
// 从PowerToys Runner启动
Logger.LogInfo($"PowerDisplay started from PowerToys Runner. Runner pid={_powerToysRunnerPid}");
// 监控父进程
RunnerHelper.WaitForPowerToysRunner(_powerToysRunnerPid, () =>
{
Logger.LogInfo("PowerToys Runner exited. Exiting PowerDisplay");
ForceExit();
});
}
}
else
{
// 独立运行模式
Logger.LogInfo("PowerDisplay started detached from PowerToys Runner.");
_powerToysRunnerPid = -1;
}
// 创建主窗口但不激活,窗口会在初始化后自动隐藏
_mainWindow = new MainWindow();
}
catch (Exception ex)
{
ShowStartupError(ex);
}
}
/// <summary>
/// 显示启动错误
/// </summary>
private void ShowStartupError(Exception ex)
{
try
{
var errorWindow = new Window
{
Title = "PowerDisplay - 启动错误"
};
var rootPanel = new StackPanel
{
Margin = new Thickness(20),
Spacing = 16
};
var titleText = new TextBlock
{
Text = "PowerDisplay 启动失败",
FontSize = 20,
FontWeight = Microsoft.UI.Text.FontWeights.SemiBold
};
var messageText = new TextBlock
{
Text = $"错误信息:{ex.Message}",
FontSize = 14,
TextWrapping = TextWrapping.Wrap
};
var detailsText = new TextBlock
{
Text = $"详细信息:\n{ex}",
FontSize = 12,
TextWrapping = TextWrapping.Wrap,
Foreground = new Microsoft.UI.Xaml.Media.SolidColorBrush(Microsoft.UI.Colors.Gray),
Margin = new Thickness(0, 10, 0, 0)
};
var closeButton = new Button
{
Content = "关闭",
HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Thickness(0, 10, 0, 0)
};
closeButton.Click += (_, _) => errorWindow.Close();
rootPanel.Children.Add(titleText);
rootPanel.Children.Add(messageText);
rootPanel.Children.Add(detailsText);
rootPanel.Children.Add(closeButton);
var scrollViewer = new ScrollViewer
{
Content = rootPanel,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
MaxHeight = 600,
MaxWidth = 800
};
errorWindow.Content = scrollViewer;
errorWindow.Activate();
}
catch
{
// 如果连错误窗口都无法显示,静默退出
Environment.Exit(1);
}
}
/// <summary>
/// 获取主窗口实例
/// </summary>
public Window? MainWindow => _mainWindow;
/// <summary>
/// 判断是否独立运行不是从PowerToys Runner启动
/// </summary>
public bool IsRunningDetachedFromPowerToys()
{
return _powerToysRunnerPid == -1;
}
/// <summary>
/// 应用程序退出时的快速清理
/// </summary>
public void Shutdown()
{
try
{
// 启动超时机制确保1秒内必须退出
var timeoutTimer = new System.Threading.Timer(_ =>
{
Logger.LogWarning("Shutdown timeout reached, forcing exit");
Environment.Exit(0);
}, null, 1000, System.Threading.Timeout.Infinite);
// 立即通知 MainWindow 程序正在退出,启用快速退出模式
if (_mainWindow is MainWindow mainWindow)
{
mainWindow.SetExiting();
mainWindow.FastShutdown(); // 新增快速关闭方法
}
_mainWindow = null;
// 立即释放 Mutex
_mutex?.ReleaseMutex();
_mutex?.Dispose();
_mutex = null;
// 取消超时计时器
timeoutTimer?.Dispose();
}
catch
{
// 忽略清理错误,确保能够退出
Environment.Exit(0);
}
}
/// <summary>
/// 强制退出应用程序,确保完全终止
/// </summary>
private void ForceExit()
{
try
{
// 立即启动超时机制500ms内必须退出
var emergencyTimer = new System.Threading.Timer(_ =>
{
Logger.LogWarning("Emergency exit timeout reached, terminating process");
Environment.Exit(0);
}, null, 500, System.Threading.Timeout.Infinite);
PerformForceExit();
}
catch
{
// 如果所有其他方法都失败,立即强制退出进程
Environment.Exit(0);
}
}
/// <summary>
/// 执行快速退出操作
/// </summary>
private void PerformForceExit()
{
try
{
// 快速关闭
Shutdown();
// 立即退出
Environment.Exit(0);
}
catch
{
// 确保能够退出
Environment.Exit(0);
}
}
}
}

View File

@@ -0,0 +1,338 @@
<?xml version="1.0" encoding="utf-8"?>
<Window x:Class="PowerDisplay.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:PowerDisplay.ViewModels"
xmlns:converters="using:PowerDisplay.Converters"
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:animations="using:CommunityToolkit.WinUI.Animations">
<Grid x:Name="RootGrid" Style="{StaticResource WindowBackgroundStyle}">
<Grid.RenderTransform>
<TranslateTransform x:Name="RootGridTransform" X="0" />
</Grid.RenderTransform>
<Grid.Resources>
<!-- Enhanced Slide-in animation storyboard -->
<Storyboard x:Key="SlideInStoryboard">
<DoubleAnimation Storyboard.TargetName="RootGridTransform"
Storyboard.TargetProperty="X"
From="300"
To="0"
Duration="0:0:0.4">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetName="RootGrid"
Storyboard.TargetProperty="Opacity"
From="0"
To="1"
Duration="0:0:0.3" />
<DoubleAnimation Storyboard.TargetName="MainContainer"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"
From="20"
To="0"
Duration="0:0:0.5">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!-- Enhanced Slide-out animation storyboard -->
<Storyboard x:Key="SlideOutStoryboard">
<DoubleAnimation Storyboard.TargetName="RootGridTransform"
Storyboard.TargetProperty="X"
From="0"
To="300"
Duration="0:0:0.3">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseIn" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetName="RootGrid"
Storyboard.TargetProperty="Opacity"
From="1"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</Grid.Resources>
<!-- Main Container with modern design -->
<Border x:Name="MainContainer"
CornerRadius="{StaticResource OverlayCornerRadius}"
Background="{ThemeResource LayerFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
Margin="0"
MaxWidth="640"
HorizontalAlignment="Stretch"
VerticalAlignment="Top">
<Border.RenderTransform>
<TranslateTransform Y="0" />
</Border.RenderTransform>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Content Area -->
<ScrollViewer Grid.Row="0"
ZoomMode="Disabled"
HorizontalScrollMode="Disabled"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto"
MaxHeight="420"
Padding="8">
<StackPanel Spacing="{StaticResource SmallSpacing}">
<!-- Loading State with modern progress -->
<StackPanel Orientation="Vertical"
Spacing="{StaticResource LargeSpacing}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Visibility="{Binding IsScanning, Converter={StaticResource BoolToVisibilityConverter}}">
<ProgressRing IsActive="True"
Width="32"
Height="32"
Foreground="{ThemeResource AccentFillColorDefaultBrush}" />
<TextBlock x:Name="ScanningMonitorsTextBlock"
Style="{StaticResource ControlLabelStyle}"
HorizontalAlignment="Center"
TextAlignment="Center" />
</StackPanel>
<!-- No Monitors State with InfoBar -->
<InfoBar x:Name="NoMonitorsInfoBar"
IsOpen="{Binding ShowNoMonitorsMessage}"
Severity="Informational"
IsClosable="False"
Background="{ThemeResource InfoBarInformationalSeverityBackgroundBrush}">
<InfoBar.IconSource>
<FontIconSource Glyph="&#xE7F4;" />
</InfoBar.IconSource>
<TextBlock x:Name="NoMonitorsTextBlock" />
</InfoBar>
<!-- Monitors List with modern card design -->
<ItemsControl ItemsSource="{Binding Monitors}"
Visibility="{Binding HasMonitors, Converter={StaticResource BoolToVisibilityConverter}}"
HorizontalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"
Spacing="0">
<StackPanel.ChildrenTransitions>
<TransitionCollection>
<EntranceThemeTransition FromVerticalOffset="20" />
<RepositionThemeTransition IsStaggeringEnabled="True" />
</TransitionCollection>
</StackPanel.ChildrenTransitions>
</StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:MainViewModel+MonitorViewModel">
<StackPanel Spacing="2" HorizontalAlignment="Stretch">
<StackPanel.ChildrenTransitions>
<TransitionCollection>
<EntranceThemeTransition FromVerticalOffset="8" />
<RepositionThemeTransition />
</TransitionCollection>
</StackPanel.ChildrenTransitions>
<!-- Monitor Name -->
<TextBlock Text="{Binding Name}"
Style="{StaticResource MonitorTitleTextStyle}"
Padding="8,4" />
<!-- Brightness Control -->
<Grid Height="40" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="7*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<FontIcon Grid.Column="0"
Glyph="&#xE793;"
Style="{StaticResource CompactIconStyle}" />
<Slider Grid.Column="1"
HorizontalAlignment="Stretch"
Style="{StaticResource CompactSliderStyle}"
Minimum="{Binding MinBrightness}"
Maximum="{Binding MaxBrightness}"
Value="{Binding Brightness, Mode=TwoWay}"
IsEnabled="{Binding IsAvailable}" />
<TextBlock Grid.Column="2"
Style="{StaticResource CompactValueTextStyle}"
Text="{Binding Brightness, Mode=OneWay}" />
</Grid>
<!-- Color Temperature Control -->
<Grid Height="40"
HorizontalAlignment="Stretch"
Visibility="{Binding ShowColorTemperature, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid.Transitions>
<TransitionCollection>
<EntranceThemeTransition FromVerticalOffset="10" />
<RepositionThemeTransition />
</TransitionCollection>
</Grid.Transitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="7*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<FontIcon Grid.Column="0"
Glyph="&#xE790;"
Style="{StaticResource CompactIconStyle}" />
<Slider Grid.Column="1"
HorizontalAlignment="Stretch"
Style="{StaticResource CompactSliderStyle}"
Minimum="0"
Maximum="100"
Value="{Binding ColorTemperaturePercent, Mode=TwoWay}"
IsEnabled="{Binding IsAvailable}" />
<TextBlock Grid.Column="2"
Style="{StaticResource CompactValueTextStyle}">
<Run Text="{Binding ColorTemperature}" />
<Run Text="K" FontSize="9" />
</TextBlock>
</Grid>
<!-- Contrast Control -->
<Grid Height="40"
HorizontalAlignment="Stretch"
Visibility="{Binding ShowContrast, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid.Transitions>
<TransitionCollection>
<EntranceThemeTransition FromVerticalOffset="10" />
<RepositionThemeTransition />
</TransitionCollection>
</Grid.Transitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="7*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<FontIcon Grid.Column="0"
Glyph="&#xE7A6;"
Style="{StaticResource CompactIconStyle}" />
<Slider Grid.Column="1"
HorizontalAlignment="Stretch"
Style="{StaticResource CompactSliderStyle}"
Minimum="0"
Maximum="100"
Value="{Binding ContrastPercent, Mode=TwoWay}"
IsEnabled="{Binding IsAvailable}" />
<TextBlock Grid.Column="2"
Style="{StaticResource CompactValueTextStyle}">
<Run Text="{Binding Contrast}" />
<Run Text="%" FontSize="9" />
</TextBlock>
</Grid>
<!-- Volume Control -->
<Grid Height="40"
HorizontalAlignment="Stretch"
Visibility="{Binding ShowVolume, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid.Transitions>
<TransitionCollection>
<EntranceThemeTransition FromVerticalOffset="10" />
<RepositionThemeTransition />
</TransitionCollection>
</Grid.Transitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="7*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<FontIcon Grid.Column="0"
Glyph="&#xE767;"
Style="{StaticResource CompactIconStyle}" />
<Slider Grid.Column="1"
HorizontalAlignment="Stretch"
Style="{StaticResource CompactSliderStyle}"
Minimum="{Binding MinVolume}"
Maximum="{Binding MaxVolume}"
Value="{Binding Volume, Mode=TwoWay}"
IsEnabled="{Binding IsAvailable}" />
<TextBlock Grid.Column="2"
Style="{StaticResource CompactValueTextStyle}">
<Run Text="{Binding Volume}" />
<Run Text="%" FontSize="9" />
</TextBlock>
</Grid>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
<!-- Status Bar with modern design -->
<Grid Grid.Row="1" Style="{StaticResource StatusBarStyle}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Status Information -->
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
<TextBlock x:Name="AdjustBrightnessTextBlock"
Style="{StaticResource MonitorTitleTextStyle}" />
<TextBlock Text="{Binding StatusText}"
Style="{StaticResource ControlLabelStyle}" />
</StackPanel>
<!-- Action Buttons -->
<StackPanel Grid.Column="1"
Orientation="Horizontal"
Spacing="{StaticResource SmallSpacing}"
VerticalAlignment="Center">
<Button x:Name="LinkButton"
Style="{StaticResource MonitorActionButtonStyle}"
ToolTipService.ToolTip="Sync all monitors">
<FontIcon Glyph="&#xE71B;" FontSize="16" />
</Button>
<Button x:Name="DisableButton"
Style="{StaticResource MonitorActionButtonStyle}"
ToolTipService.ToolTip="Toggle control">
<FontIcon Glyph="&#xE7E8;" FontSize="16" />
</Button>
<Button x:Name="ThemeButton"
Style="{StaticResource MonitorActionButtonStyle}"
ToolTipService.ToolTip="Switch theme">
<FontIcon x:Name="ThemeIcon"
Glyph="&#xE706;"
FontSize="16" />
</Button>
<Button x:Name="RefreshButton"
Style="{StaticResource MonitorActionButtonStyle}"
ToolTipService.ToolTip="Refresh monitors">
<FontIcon Glyph="&#xE72C;" FontSize="16" />
</Button>
</StackPanel>
</Grid>
</Grid>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,735 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Management;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Common.UI;
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Composition;
using PowerDisplay.Core;
using PowerDisplay.Core.Interfaces;
using PowerDisplay.Core.Models;
using PowerDisplay.Helpers;
using PowerDisplay.Native;
using PowerDisplay.ViewModels;
using Windows.Graphics;
using WinRT.Interop;
using Monitor = PowerDisplay.Core.Models.Monitor;
namespace PowerDisplay
{
/// <summary>
/// PowerDisplay main window
/// </summary>
public sealed partial class MainWindow : Window
{
private MainViewModel _viewModel = null!;
private TrayIconHelper _trayIcon = null!;
private AppWindow _appWindow = null!;
private bool _isExiting = false;
private readonly ISettingsUtils _settingsUtils = new SettingsUtils();
public MainWindow()
{
try
{
this.InitializeComponent();
// Initialize ViewModel and bind to root Grid
_viewModel = new MainViewModel();
RootGrid.DataContext = _viewModel;
// Initialize ViewModel event handlers
_viewModel.UIRefreshRequested += OnUIRefreshRequested;
_viewModel.Monitors.CollectionChanged += OnMonitorsCollectionChanged;
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
_viewModel.ThemeChangeRequested += OnThemeChangeRequested;
// Bind button events
LinkButton.Click += OnLinkClick;
DisableButton.Click += OnDisableClick;
ThemeButton.Click += OnThemeButtonClick;
RefreshButton.Click += OnRefreshClick;
// Setup window properties
SetupWindow();
// Initialize theme and icons
InitializeTheme();
// Initialize tray icon
InitializeTrayIcon();
// Clean up resources on window close
this.Closed += OnWindowClosed;
// Initialize UI text
InitializeUIText();
// Initialize on startup
_ = InitializeAsync();
// Hide window on startup, show only tray icon
_ = Task.Run(async () =>
{
await Task.Delay(100); // Wait for window to fully initialize
DispatcherQueue.TryEnqueue(() => HideWindow());
});
}
catch (Exception e)
{
ShowError($"Unable to start main window: {e.Message}");
}
}
private async Task InitializeAsync()
{
try
{
await Task.Delay(500);
await _viewModel.RefreshMonitorsAsync();
_viewModel.ReloadMonitorSettings();
// Delay to allow UI to render, then adjust window size
await Task.Delay(100);
DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent());
}
catch (System.Management.ManagementException)
{
ShowError("Unable to access internal display control, administrator privileges may be required.");
}
catch (Exception ex)
{
ShowError($"Initialization failed: {ex.Message}");
}
}
private void InitializeUIText()
{
try
{
var loader = ResourceLoaderInstance.ResourceLoader;
// Set text block content
ScanningMonitorsTextBlock.Text = loader.GetString("ScanningMonitorsText");
NoMonitorsTextBlock.Text = loader.GetString("NoMonitorsText");
AdjustBrightnessTextBlock.Text = loader.GetString("AdjustBrightnessText");
// Set button tooltips
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(LinkButton, loader.GetString("SyncAllMonitorsTooltip"));
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(DisableButton, loader.GetString("ToggleControlTooltip"));
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(ThemeButton, loader.GetString("ToggleThemeTooltip"));
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(RefreshButton, loader.GetString("RefreshTooltip"));
}
catch
{
// Use English defaults if resource loading fails
ScanningMonitorsTextBlock.Text = "Scanning monitors...";
NoMonitorsTextBlock.Text = "No monitors detected";
AdjustBrightnessTextBlock.Text = "Adjust Brightness";
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(LinkButton, "Synchronize all monitors to the same brightness");
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(DisableButton, "Enable or disable brightness control");
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(ThemeButton, "Switch between light and dark themes");
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(RefreshButton, "Rescan connected monitors");
}
}
private void ShowError(string message)
{
_viewModel.StatusText = $"Error: {message}";
}
private void OnWindowClosed(object sender, WindowEventArgs args)
{
// Allow window to close if program is exiting
if (_isExiting)
{
// Clean up event subscriptions
if (_viewModel != null)
{
_viewModel.UIRefreshRequested -= OnUIRefreshRequested;
_viewModel.Monitors.CollectionChanged -= OnMonitorsCollectionChanged;
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
_viewModel.ThemeChangeRequested -= OnThemeChangeRequested;
}
args.Handled = false;
return;
}
// If only user operation (although we hide close button), just hide window
args.Handled = true; // Prevent window closing
HideWindow();
}
private void InitializeTrayIcon()
{
_trayIcon = new TrayIconHelper(this);
_trayIcon.SetCallbacks(
onShow: ShowWindow,
onExit: ExitApplication,
onRefresh: () => _viewModel?.RefreshCommand?.Execute(null),
onSettings: OpenSettings
);
}
private void OpenSettings()
{
try
{
// Open PowerToys Settings to PowerDisplay page
// Use true for WinUI 3 apps as PowerToys.exe is in parent directory
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.PowerDisplay, true);
}
catch (Exception ex)
{
Logger.LogError($"Failed to open settings: {ex.Message}");
}
}
private void ShowWindow()
{
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
// Adjust window size before showing
AdjustWindowSizeToContent();
// Reposition to bottom right (set position before showing)
if (_appWindow != null)
{
PositionWindowAtBottomRight(_appWindow);
}
// Set initial state for animation
RootGrid.Opacity = 0;
// Show window
WindowHelper.ShowWindow(hWnd, true);
// Bring window to foreground
WindowHelper.SetForegroundWindow(hWnd);
// Use storyboard animation for window entrance
if (RootGrid.Resources.ContainsKey("SlideInStoryboard"))
{
var slideInStoryboard = (Storyboard)RootGrid.Resources["SlideInStoryboard"];
slideInStoryboard.Begin();
}
}
private void HideWindow()
{
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
// Use storyboard animation for window exit
if (RootGrid.Resources.ContainsKey("SlideOutStoryboard"))
{
var slideOutStoryboard = (Storyboard)RootGrid.Resources["SlideOutStoryboard"];
slideOutStoryboard.Completed += (s, e) =>
{
// Hide window after animation completes
WindowHelper.ShowWindow(hWnd, false);
};
slideOutStoryboard.Begin();
}
else
{
// Fallback: hide immediately if animation not found
WindowHelper.ShowWindow(hWnd, false);
}
}
private void OnUIRefreshRequested(object? sender, EventArgs e)
{
Logger.LogInfo("UI refresh requested due to settings change");
_viewModel.ReloadMonitorSettings();
// Adjust window size after settings change to accommodate visibility changes
_ = Task.Run(async () =>
{
await Task.Delay(100); // Allow UI to update with new visibility settings
DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent());
});
}
private void OnThemeChangeRequested(object? sender, ElementTheme theme)
{
Logger.LogInfo($"Theme change requested: {theme}");
ApplyThemeFromSettings(theme);
}
private void ApplyThemeFromSettings(ElementTheme theme)
{
try
{
// Apply theme to window
PowerDisplay.Helpers.ThemeManager.ApplyTheme(this, theme);
// Update theme icon
UpdateThemeIcon(theme == ElementTheme.Dark);
Logger.LogInfo($"Theme applied: {theme}");
}
catch (Exception ex)
{
Logger.LogError($"Failed to apply theme: {ex.Message}");
}
}
private void OnMonitorsCollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
// Adjust window size when monitors collection changes (add/remove monitors)
_ = Task.Run(async () =>
{
await Task.Delay(100); // Small delay to allow UI to update
DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent());
});
}
private void OnViewModelPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
// Adjust window size when relevant properties change
if (e.PropertyName == nameof(_viewModel.IsScanning) ||
e.PropertyName == nameof(_viewModel.HasMonitors) ||
e.PropertyName == nameof(_viewModel.ShowNoMonitorsMessage))
{
_ = Task.Run(async () =>
{
await Task.Delay(50); // Small delay to allow UI to update
DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent());
});
}
}
/// <summary>
/// Set exit flag to allow window to close normally
/// </summary>
public void SetExiting()
{
_isExiting = true;
}
/// <summary>
/// 快速关闭窗口,跳过动画和复杂清理
/// </summary>
public void FastShutdown()
{
try
{
_isExiting = true;
// 立即释放托盘图标
_trayIcon?.Dispose();
// 快速清理 ViewModel
if (_viewModel != null)
{
// 取消事件订阅
_viewModel.UIRefreshRequested -= OnUIRefreshRequested;
_viewModel.Monitors.CollectionChanged -= OnMonitorsCollectionChanged;
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
_viewModel.ThemeChangeRequested -= OnThemeChangeRequested;
// 立即释放
_viewModel.Dispose();
}
// 直接关闭窗口,不等待动画
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
WindowHelper.ShowWindow(hWnd, false);
}
catch
{
// 忽略清理错误,确保能够关闭
}
}
private void ExitApplication()
{
try
{
// 使用快速关闭
FastShutdown();
// 直接调用应用程序快速退出
if (Application.Current is App app)
{
app.Shutdown();
}
// 确保立即退出
Environment.Exit(0);
}
catch
{
// 确保能够退出
Environment.Exit(0);
}
}
private async void OnRefreshClick(object sender, RoutedEventArgs e)
{
// Add button press animation
if (sender is Button button)
{
await AnimateButtonPress(button);
}
// Refresh monitor list
if (_viewModel?.RefreshCommand?.CanExecute(null) == true)
{
_viewModel.RefreshCommand.Execute(null);
// Adjust window size after refresh
_ = Task.Run(async () =>
{
await Task.Delay(200); // Allow data to load
DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent());
});
}
}
private async void OnThemeButtonClick(object sender, RoutedEventArgs e)
{
// Add button press animation
if (sender is Button button)
{
await AnimateButtonPress(button);
}
var currentTheme = PowerDisplay.Helpers.ThemeManager.GetCurrentTheme(this);
var newTheme = currentTheme == ElementTheme.Light ? ElementTheme.Dark : ElementTheme.Light;
// Apply theme and sync to both settings systems
PowerDisplay.Helpers.ThemeManager.ApplyThemeAndSync(this, newTheme);
UpdateThemeIcon(newTheme == ElementTheme.Dark);
Logger.LogInfo($"Theme toggled to: {newTheme}");
}
private void UpdateThemeIcon(bool isDark)
{
if (ThemeIcon != null)
{
// Dark theme shows sun icon, light theme shows moon icon
ThemeIcon.Glyph = isDark ? "\uE706" : "\uE708";
}
}
private void InitializeTheme()
{
// Load saved theme settings (with PowerToys settings priority)
var savedTheme = PowerDisplay.Helpers.ThemeManager.GetSavedThemeWithPriority();
if (savedTheme != ElementTheme.Default)
{
PowerDisplay.Helpers.ThemeManager.ApplyTheme(this, savedTheme);
}
// Update theme icon
var isDark = PowerDisplay.Helpers.ThemeManager.IsDarkTheme(this);
UpdateThemeIcon(isDark);
}
private async void OnLinkClick(object sender, RoutedEventArgs e)
{
// Add button press animation
if (sender is Button button)
{
await AnimateButtonPress(button);
}
// Link all monitor brightness (synchronized adjustment)
if (_viewModel != null && _viewModel.Monitors.Count > 0)
{
// Get first monitor brightness as reference
var baseBrightness = _viewModel.Monitors.First().Brightness;
_ = _viewModel.SetAllBrightnessAsync(baseBrightness);
}
}
private async void OnDisableClick(object sender, RoutedEventArgs e)
{
// Add button press animation
if (sender is Button button)
{
await AnimateButtonPress(button);
}
// Disable/enable all monitor controls
if (_viewModel != null)
{
foreach (var monitor in _viewModel.Monitors)
{
monitor.IsAvailable = !monitor.IsAvailable;
}
_viewModel.StatusText = _viewModel.Monitors.Any(m => m.IsAvailable)
? "Display control enabled"
: "Display control disabled";
}
}
/// <summary>
/// Get internal monitor name, consistent with SettingsManager logic
/// </summary>
private async void OnTestClick(object sender, RoutedEventArgs e)
{
// Test monitor discovery functionality
var dlg = new ContentDialog
{
Title = "Monitor Detection Test",
Content = "Starting monitor detection...",
CloseButtonText = "Close",
XamlRoot = this.Content.XamlRoot
};
_ = dlg.ShowAsync();
try
{
var manager = new Core.MonitorManager();
var monitors = await manager.DiscoverMonitorsAsync(new System.Threading.CancellationToken());
string message = $"Found {monitors.Count} monitors:\n\n";
foreach (var monitor in monitors)
{
message += $"• {monitor.Name}\n";
message += $" Type: {monitor.Type}\n";
message += $" Brightness: {monitor.CurrentBrightness}%\n\n";
}
if (monitors.Count == 0)
{
message = "No monitors found.\n\n";
message += "Possible reasons:\n";
message += "• DDC/CI not supported\n";
message += "• Driver issues\n";
message += "• Permission issues\n";
message += "• Cable doesn't support DDC/CI";
}
dlg.Content = message;
// Don't dispose manager, use existing manager
// Initialize ViewModel and bind to root Grid refresh
if (monitors.Count > 0)
{
// Use existing refresh command
await _viewModel.RefreshMonitorsAsync(); }
manager.Dispose();
}
catch (Exception ex)
{
dlg.Content = $"Error: {ex.Message}\n\nType: {ex.GetType().Name}";
}
}
private void SetupWindow()
{
try
{
// Get window handle
var hWnd = WindowNative.GetWindowHandle(this);
var windowId = Win32Interop.GetWindowIdFromWindow(hWnd);
_appWindow = AppWindow.GetFromWindowId(windowId);
if (_appWindow != null)
{
// Set initial window size - will be adjusted later based on content
_appWindow.Resize(new SizeInt32 { Width = 640, Height = 480 });
// Position window at bottom right corner
PositionWindowAtBottomRight(_appWindow);
// Set window icon and title bar
_appWindow.Title = "PowerDisplay";
// Remove title bar and system buttons
var presenter = _appWindow.Presenter as OverlappedPresenter;
if (presenter != null)
{
// Disable resizing
presenter.IsResizable = false;
// Disable maximize button
presenter.IsMaximizable = false;
// Disable minimize button
presenter.IsMinimizable = false;
// Set borderless mode
presenter.SetBorderAndTitleBar(false, false);
}
// Custom title bar - completely remove all buttons
var titleBar = _appWindow.TitleBar;
if (titleBar != null)
{
// Extend content into title bar area
titleBar.ExtendsContentIntoTitleBar = true;
// Completely remove title bar height
titleBar.PreferredHeightOption = Microsoft.UI.Windowing.TitleBarHeightOption.Collapsed;
// Set all button colors to transparent
titleBar.ButtonBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
titleBar.ButtonInactiveBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
titleBar.ButtonForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
titleBar.ButtonHoverBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
titleBar.ButtonHoverForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
titleBar.ButtonPressedBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
titleBar.ButtonPressedForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
titleBar.ButtonInactiveForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
// Disable title bar interaction area
titleBar.SetDragRectangles(new Windows.Graphics.RectInt32[0]);
}
// Set modern Mica Alt backdrop for Windows 11
try
{
// Use Mica Alt for a more modern appearance
if (Microsoft.UI.Composition.SystemBackdrops.MicaController.IsSupported())
{
this.SystemBackdrop = new Microsoft.UI.Xaml.Media.MicaBackdrop();
}
else
{
// Fallback to basic backdrop for older systems
this.SystemBackdrop = new Microsoft.UI.Xaml.Media.DesktopAcrylicBackdrop();
}
}
catch
{
// Fallback: use solid color background
this.SystemBackdrop = null;
}
// Use Win32 API to further disable window moving
WindowHelper.DisableWindowMovingAndResizing(hWnd);
// Hide window from taskbar
WindowHelper.HideFromTaskbar(hWnd);
// Optional: set window topmost
// WindowHelper.SetWindowTopmost(hWnd, true);
}
}
catch
{
// Ignore window setup errors
}
}
private void AdjustWindowSizeToContent()
{
try
{
if (_appWindow == null || RootGrid == null)
return;
// Force layout update to ensure proper measurement
RootGrid.UpdateLayout();
// Get precise content height
var availableWidth = 640.0;
var contentHeight = GetContentHeight(availableWidth);
// Account for display scaling
var scale = RootGrid.XamlRoot?.RasterizationScale ?? 1.0;
var scaledHeight = (int)Math.Ceiling(contentHeight * scale);
// Only set maximum height for scrollable content
scaledHeight = Math.Min(scaledHeight, 650);
// Check if resize is needed
var currentSize = _appWindow.Size;
if (Math.Abs(currentSize.Height - scaledHeight) > 1)
{
Logger.LogInfo($"Adjusting window height from {currentSize.Height} to {scaledHeight} (content: {contentHeight})");
_appWindow.Resize(new SizeInt32 { Width = 640, Height = scaledHeight });
// Update clip region to match new window size
UpdateClipRegion(640, scaledHeight / scale);
// Reposition to maintain bottom-right position
PositionWindowAtBottomRight(_appWindow);
}
}
catch (Exception ex)
{
Logger.LogError($"Error adjusting window size: {ex.Message}");
}
}
private double GetContentHeight(double availableWidth)
{
// Try to measure MainContainer directly for precise content size
if (RootGrid.FindName("MainContainer") is Border mainContainer)
{
mainContainer.Measure(new Windows.Foundation.Size(availableWidth, double.PositiveInfinity));
return mainContainer.DesiredSize.Height;
}
// Fallback: Measure the root grid
RootGrid.Measure(new Windows.Foundation.Size(availableWidth, double.PositiveInfinity));
return RootGrid.DesiredSize.Height + 4; // Small padding for fallback method
}
private void UpdateClipRegion(double width, double height)
{
// Clip region removed to allow automatic sizing
// No longer needed as we removed the fixed clip from RootGrid
}
private void PositionWindowAtBottomRight(AppWindow appWindow)
{
try
{
// Get display area
var displayArea = DisplayArea.GetFromWindowId(appWindow.Id, DisplayAreaFallback.Nearest);
if (displayArea != null)
{
var workArea = displayArea.WorkArea;
var windowSize = appWindow.Size;
// Calculate bottom-right position, close to taskbar
// WorkArea already excludes taskbar area, so use WorkArea bottom directly
int rightMargin = 10; // Small margin from right edge
int x = workArea.Width - windowSize.Width - rightMargin;
int y = workArea.Height - windowSize.Height; // Close to taskbar top, no gap
// Move window to bottom right
appWindow.Move(new PointInt32 { X = x, Y = y });
}
}
catch
{
// Ignore errors when positioning window
}
}
/// <summary>
/// Animates button press for modern interaction feedback
/// </summary>
/// <param name="button">The button to animate</param>
private async Task AnimateButtonPress(Button button)
{
// Button animation disabled to avoid compilation errors
// Using default button visual states instead
await Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ScanningMonitorsText" xml:space="preserve">
<value>Scanning monitors...</value>
</data>
<data name="NoMonitorsText" xml:space="preserve">
<value>No monitors detected</value>
</data>
<data name="AdjustBrightnessText" xml:space="preserve">
<value>Adjust Brightness</value>
</data>
</root>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ScanningMonitorsText" xml:space="preserve">
<value>正在扫描显示器...</value>
</data>
<data name="NoMonitorsText" xml:space="preserve">
<value>未检测到显示器</value>
</data>
<data name="AdjustBrightnessText" xml:space="preserve">
<value>调节亮度</value>
</data>
</root>

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace PowerDisplay.Telemetry.Events
{
[EventData]
public class PowerDisplayStartEvent : EventBase, IEvent
{
public new string EventName => "PowerDisplay_Start";
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity version="1.0.0.0" name="PowerDisplay.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
<!-- Windows 11 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@@ -0,0 +1,7 @@
#include <string>
namespace PowerDisplayConstants
{
// Name of the powertoy module.
inline const std::wstring ModuleKey = L"PowerDisplay";
}

View File

@@ -0,0 +1,108 @@
// Microsoft Visual C++ generated resource script.
//
#include <windows.h>
#include "resource.h"
#include "../../../common/version/version.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
1 VERSIONINFO
FILEVERSION FILE_VERSION
PRODUCTVERSION PRODUCT_VERSION
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
BEGIN
VALUE "CompanyName", COMPANY_NAME
VALUE "FileDescription", FILE_DESCRIPTION
VALUE "FileVersion", FILE_VERSION_STRING
VALUE "InternalName", INTERNAL_NAME
VALUE "LegalCopyright", COPYRIGHT_NOTE
VALUE "OriginalFilename", ORIGINAL_FILENAME
VALUE "ProductName", PRODUCT_NAME
VALUE "ProductVersion", PRODUCT_VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
END
END
/////////////////////////////////////////////////////////////////////////////
// English (United States) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// String Table
//
STRINGTABLE
BEGIN
IDS_REGISTRYPREVIEW_NAME "RegistryPreview"
END
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED

View File

@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{D1234567-8901-2345-6789-ABCDEF012345}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>PowerDisplayExt</RootNamespace>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup>
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\</OutDir>
<TargetName>PowerToys.PowerDisplayExt</TargetName>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;POWERDISPLAYEXT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalDependencies>Shlwapi.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;POWERDISPLAYEXT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalDependencies>Shlwapi.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="Constants.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="trace.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="trace.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="PowerDisplayExt.rc" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Constants.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Trace.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Trace.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="RegistryPreviewExt.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,32 @@
#include "pch.h"
#include "trace.h"
#include <common/Telemetry/TraceBase.h>
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
"Microsoft.PowerToys",
// {38e8889b-9731-53f5-e901-e8a7c1753074}
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
TraceLoggingOptionProjectTelemetry());
// Log if the user has enabled or disabled the app
void Trace::EnableRegistryPreview(_In_ bool enabled) noexcept
{
TraceLoggingWriteWrapper(
g_hProvider,
"RegistryPreview_EnableRegistryPreview",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(enabled, "Enabled"));
}
// Log that the user tried to activate the app
void Trace::ActivateEditor() noexcept
{
TraceLoggingWriteWrapper(
g_hProvider,
"RegistryPreview_Activate",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}

View File

@@ -0,0 +1,13 @@
#pragma once
#include <common/Telemetry/TraceBase.h>
class Trace : public telemetry::TraceBase
{
public:
// Log if the user has enabled or disabled the app
static void EnableRegistryPreview(const bool enabled) noexcept;
// Log that the user tried to activate the app
static void ActivateEditor() noexcept;
};

View File

@@ -0,0 +1,387 @@
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include <interface/powertoy_module_interface.h>
#include <common/SettingsAPI/settings_objects.h>
#include "trace.h"
#include <common/interop/shared_constants.h>
#include <common/utils/string_utils.h>
#include <common/utils/winapi_error.h>
#include <common/utils/logger_helper.h>
#include <common/utils/resources.h>
#include <common/utils/process_path.h>
#include "resource.h"
#include "Constants.h"
extern "C" IMAGE_DOS_HEADER __ImageBase;
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Trace::RegisterProvider();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
Trace::UnregisterProvider();
break;
}
return TRUE;
}
const static wchar_t* MODULE_NAME = L"PowerDisplay";
const static wchar_t* MODULE_DESC = L"A utility to manage display brightness and color temperature across multiple monitors.";
namespace
{
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
const wchar_t JSON_KEY_ENABLED[] = L"enabled";
const wchar_t JSON_KEY_HOTKEY_ENABLED[] = L"hotkey_enabled";
}
class PowerDisplayModule : public PowertoyModuleIface
{
private:
bool m_enabled = false;
bool m_hotkey_enabled = false;
PROCESS_INFORMATION p_info = {};
bool is_process_running()
{
return WaitForSingleObject(p_info.hProcess, 0) == WAIT_TIMEOUT;
}
bool graceful_shutdown_process()
{
if (!is_process_running())
{
return true; // Already not running
}
try
{
// Send WM_CLOSE message to the main window
DWORD processId = GetProcessId(p_info.hProcess);
if (processId != 0)
{
// Find the main window of the PowerDisplay process
HWND hwnd = find_main_window(processId);
if (hwnd != NULL)
{
Logger::trace(L"Sending WM_CLOSE to PowerDisplay window");
PostMessage(hwnd, WM_CLOSE, 0, 0);
// Wait up to 2 seconds for graceful shutdown
DWORD wait_result = WaitForSingleObject(p_info.hProcess, 2000);
if (wait_result == WAIT_OBJECT_0)
{
return true; // Process exited gracefully
}
// If WM_CLOSE didn't work, try WM_QUIT
Logger::trace(L"WM_CLOSE failed, trying WM_QUIT");
PostMessage(hwnd, WM_QUIT, 0, 0);
wait_result = WaitForSingleObject(p_info.hProcess, 1000);
if (wait_result == WAIT_OBJECT_0)
{
return true;
}
}
}
}
catch (...)
{
Logger::error(L"Exception during graceful shutdown attempt");
}
return false; // Graceful shutdown failed
}
struct EnumWindowsData
{
DWORD processId;
HWND foundWindow;
};
static BOOL CALLBACK enum_windows_callback(HWND hwnd, LPARAM lParam)
{
EnumWindowsData* data = reinterpret_cast<EnumWindowsData*>(lParam);
DWORD windowProcessId;
GetWindowThreadProcessId(hwnd, &windowProcessId);
if (windowProcessId == data->processId)
{
// Check if this is a main window (visible and has no parent)
if (IsWindowVisible(hwnd) && GetParent(hwnd) == NULL)
{
wchar_t className[256];
GetClassName(hwnd, className, sizeof(className) / sizeof(wchar_t));
// Look for WinUI3 window class or PowerDisplay specific window
if (wcsstr(className, L"WindowsForms") ||
wcsstr(className, L"WinUIDesktopWin32WindowClass") ||
wcsstr(className, L"PowerDisplay"))
{
data->foundWindow = hwnd;
return FALSE; // Stop enumeration
}
}
}
return TRUE; // Continue enumeration
}
HWND find_main_window(DWORD processId)
{
EnumWindowsData data = { processId, NULL };
EnumWindows(enum_windows_callback, reinterpret_cast<LPARAM>(&data));
return data.foundWindow;
}
void launch_process()
{
if (m_enabled)
{
Logger::trace(L"Starting Power Display process");
unsigned long powertoys_pid = GetCurrentProcessId();
std::wstring executable_args = L"--pid " + std::to_wstring(powertoys_pid);
std::wstring application_path = L"WinUI3Apps\\PowerToys.PowerDisplay.exe";
std::wstring full_command_path = application_path + L" " + executable_args;
Logger::trace(L"PowerDisplay launching with parameters: " + executable_args);
STARTUPINFO info = { sizeof(info) };
if (!CreateProcess(NULL, full_command_path.data(), NULL, NULL, true, NULL, NULL, NULL, &info, &p_info))
{
DWORD error = GetLastError();
std::wstring message = L"PowerDisplay failed to start with error: ";
message += std::to_wstring(error);
Logger::error(message);
}
else
{
Logger::trace("Successfully started the PowerDisplay process");
}
}
}
void parse_hotkey_settings(PowerToysSettings::PowerToyValues settings)
{
auto settingsObject = settings.get_raw_json();
if (settingsObject.GetView().Size())
{
try
{
auto hotkey_enabled = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedBoolean(JSON_KEY_HOTKEY_ENABLED);
m_hotkey_enabled = hotkey_enabled;
}
catch (...)
{
Logger::info("Failed to parse hotkey settings, using defaults");
m_hotkey_enabled = false; // Use default value
}
}
else
{
Logger::info("Power Display settings are empty");
m_hotkey_enabled = false; // Use default value
}
}
// Load the settings file.
void init_settings()
{
try
{
// Load and parse the settings file for this PowerToy.
PowerToysSettings::PowerToyValues settings =
PowerToysSettings::PowerToyValues::load_from_settings_file(get_key());
parse_hotkey_settings(settings);
}
catch (std::exception&)
{
Logger::error("Invalid json when trying to load the Power Display settings json from file.");
}
}
public:
PowerDisplayModule()
{
LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", "PowerDisplay");
Logger::info("Power Display object is constructing");
init_settings();
}
~PowerDisplayModule()
{
if (m_enabled)
{
TerminateProcess(p_info.hProcess, 1);
CloseHandle(p_info.hProcess);
CloseHandle(p_info.hThread);
}
m_enabled = false;
}
// Destroy the powertoy and free memory
virtual void destroy() override
{
delete this;
}
// Return the localized display name of the powertoy
virtual const wchar_t* get_name() override
{
return MODULE_NAME;
}
// Return the non localized key of the powertoy, this will be cached by the runner
virtual const wchar_t* get_key() override
{
return MODULE_NAME;
}
// Return the configured status for the gpo policy for the module
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
{
return powertoys_gpo::gpo_rule_configured_not_configured;
}
// Return JSON with the configuration options.
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
{
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
// Create a Settings object.
PowerToysSettings::Settings settings(hinstance, get_name());
settings.set_description(MODULE_DESC);
return settings.serialize_to_buffer(buffer, buffer_size);
}
// Pop open the app, if the OOBE page asks it to
virtual void call_custom_action(const wchar_t* action) override
{
try
{
PowerToysSettings::CustomActionObject action_object =
PowerToysSettings::CustomActionObject::from_json_string(action);
if (action_object.get_name() == L"Launch")
{
if (is_process_running())
{
Logger::trace(L"PowerDisplay process is already running. Skipping launch.");
}
else
{
launch_process();
}
Trace::ActivateEditor();
}
}
catch (std::exception&)
{
Logger::error(L"Failed to parse action. {}", action);
}
}
// Called by the runner to pass the updated settings values as a serialized JSON.
virtual void set_config(const wchar_t* config) override
{
try
{
// Parse the input JSON string.
PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
parse_hotkey_settings(values);
values.save_to_settings_file();
}
catch (std::exception&)
{
Logger::error(L"Invalid json when trying to parse Power Display settings json.");
}
}
// Enable the powertoy
virtual void enable()
{
m_enabled = true;
if (!is_process_running())
{
launch_process(); // Start the PowerDisplay process
}
else
{
Logger::trace(L"PowerDisplay process is already running. Skipping launch on enable.");
}
Logger::trace(L"PowerDisplay enabled");
};
virtual void disable()
{
if (m_enabled)
{
Logger::trace(L"Disabling Power Display...");
// Try graceful shutdown first
if (graceful_shutdown_process())
{
Logger::trace(L"PowerDisplay shutdown gracefully");
}
else
{
Logger::trace(L"PowerDisplay graceful shutdown failed, forcing termination");
// Fallback to force termination
TerminateProcess(p_info.hProcess, 1);
}
// Clean up handles
CloseHandle(p_info.hProcess);
CloseHandle(p_info.hThread);
}
m_enabled = false;
}
// Returns if the powertoys is enabled
virtual bool is_enabled() override
{
return m_enabled;
}
// Respond to a "click" from the launcher
virtual bool on_hotkey(size_t /*hotkeyId*/) override
{
if (m_enabled)
{
Logger::trace(L"Power Display hotkey pressed");
if (is_process_running())
{
TerminateProcess(p_info.hProcess, 1);
}
else
{
launch_process();
}
return true;
}
return false;
}
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new PowerDisplayModule();
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
</packages>

View File

@@ -0,0 +1 @@
#include "pch.h"

View File

@@ -0,0 +1,15 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
//#include <winrt/Windows.Foundation.h>
#include <strsafe.h>
#include <hIdUsage.h>
#include <shellapi.h>
#include <thread>
#include <winrt/Windows.Foundation.Collections.h>
//#include <Shlwapi.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/logger/logger.h>

View File

@@ -0,0 +1,21 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by Awake.rc
//
#define IDS_REGISTRYPREVIEW_NAME 101
#define FILE_DESCRIPTION "PowerToys Registry Preview Module"
#define INTERNAL_NAME "PowerToys.RegistryPreview"
#define ORIGINAL_FILENAME "PowerToys.RegistryPreview.dll"
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif

View File

@@ -177,6 +177,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
L"PowerToys.WorkspacesModuleInterface.dll",
L"PowerToys.CmdPalModuleInterface.dll",
L"PowerToys.ZoomItModuleInterface.dll",
L"PowerToys.PowerDisplayExt.dll",
};
for (auto moduleSubdir : knownModules)

View File

@@ -807,6 +807,8 @@ std::string ESettingsWindowNames_to_string(ESettingsWindowNames value)
return "CmdPal";
case ESettingsWindowNames::ZoomIt:
return "ZoomIt";
case ESettingsWindowNames::PowerDisplay:
return "PowerDisplay";
default:
{
Logger::error(L"Can't convert ESettingsWindowNames value={} to string", static_cast<int>(value));
@@ -942,6 +944,10 @@ ESettingsWindowNames ESettingsWindowNames_from_string(std::string value)
{
return ESettingsWindowNames::ZoomIt;
}
else if (value == "PowerDisplay")
{
return ESettingsWindowNames::PowerDisplay;
}
else
{
Logger::error(L"Can't convert string value={} to ESettingsWindowNames", winrt::to_hstring(value));

View File

@@ -35,6 +35,7 @@ enum class ESettingsWindowNames
NewPlus,
CmdPal,
ZoomIt,
PowerDisplay,
};
std::string ESettingsWindowNames_to_string(ESettingsWindowNames value);

View File

@@ -513,6 +513,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
private bool powerDisplay;
[JsonPropertyName("PowerDisplay")]
public bool PowerDisplay
{
get => powerDisplay;
set
{
if (powerDisplay != value)
{
LogTelemetryEvent(value);
powerDisplay = value;
NotifyChange();
}
}
}
private void NotifyChange()
{
notifyEnabledChangedAction?.Invoke();

View File

@@ -0,0 +1,201 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class MonitorInfo : Observable
{
private string _name = string.Empty;
private string _internalName = string.Empty;
private string _hardwareId = string.Empty;
private string _communicationMethod = string.Empty;
private string _monitorType = string.Empty;
private int _currentBrightness = 0;
private int _colorTemperature = 6500;
private bool _isHidden = false;
private bool _enableColorTemperature = false;
private bool _enableContrast = false;
private bool _enableVolume = false;
public MonitorInfo()
{
}
public MonitorInfo(string name, string internalName, string communicationMethod)
{
Name = name;
InternalName = internalName;
CommunicationMethod = communicationMethod;
}
public MonitorInfo(string name, string internalName, string hardwareId, string communicationMethod, string monitorType, int currentBrightness, int colorTemperature)
{
Name = name;
InternalName = internalName;
HardwareId = hardwareId;
CommunicationMethod = communicationMethod;
MonitorType = monitorType;
CurrentBrightness = currentBrightness;
ColorTemperature = colorTemperature;
}
[JsonPropertyName("name")]
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("internalName")]
public string InternalName
{
get => _internalName;
set
{
if (_internalName != value)
{
_internalName = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("communicationMethod")]
public string CommunicationMethod
{
get => _communicationMethod;
set
{
if (_communicationMethod != value)
{
_communicationMethod = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("hardwareId")]
public string HardwareId
{
get => _hardwareId;
set
{
if (_hardwareId != value)
{
_hardwareId = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("monitorType")]
public string MonitorType
{
get => _monitorType;
set
{
if (_monitorType != value)
{
_monitorType = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("currentBrightness")]
public int CurrentBrightness
{
get => _currentBrightness;
set
{
if (_currentBrightness != value)
{
_currentBrightness = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("colorTemperature")]
public int ColorTemperature
{
get => _colorTemperature;
set
{
if (_colorTemperature != value)
{
_colorTemperature = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("isHidden")]
public bool IsHidden
{
get => _isHidden;
set
{
if (_isHidden != value)
{
_isHidden = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("enableColorTemperature")]
public bool EnableColorTemperature
{
get => _enableColorTemperature;
set
{
if (_enableColorTemperature != value)
{
System.Diagnostics.Debug.WriteLine($"[MonitorInfo] EnableColorTemperature changing from {_enableColorTemperature} to {value} for monitor {Name}");
_enableColorTemperature = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("enableContrast")]
public bool EnableContrast
{
get => _enableContrast;
set
{
if (_enableContrast != value)
{
_enableContrast = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("enableVolume")]
public bool EnableVolume
{
get => _enableVolume;
set
{
if (_enableVolume != value)
{
_enableVolume = value;
OnPropertyChanged();
}
}
}
}
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library
{
/// <summary>
/// Saved settings for a monitor that can be restored on startup
/// </summary>
public class MonitorSavedSettings
{
[JsonPropertyName("brightness")]
public int Brightness { get; set; } = 30;
[JsonPropertyName("color_temperature")]
public int ColorTemperature { get; set; } = 6500;
[JsonPropertyName("contrast")]
public int Contrast { get; set; } = 50;
[JsonPropertyName("volume")]
public int Volume { get; set; } = 50;
[JsonPropertyName("last_updated")]
public DateTime LastUpdated { get; set; } = DateTime.Now;
public MonitorSavedSettings()
{
}
public MonitorSavedSettings(int brightness, int colorTemperature, int contrast, int volume)
{
Brightness = brightness;
ColorTemperature = colorTemperature;
Contrast = contrast;
Volume = volume;
LastUpdated = DateTime.Now;
}
}
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class PowerDisplayProperties
{
public PowerDisplayProperties()
{
LaunchAtStartup = false;
Theme = "Light";
BrightnessUpdateRate = "1s";
Monitors = new List<MonitorInfo>();
RestoreSettingsOnStartup = true;
SavedMonitorSettings = new Dictionary<string, MonitorSavedSettings>();
EnableMcpServer = false;
McpServerPort = 5000;
McpAutoStart = false;
}
[JsonPropertyName("launch_at_startup")]
public bool LaunchAtStartup { get; set; }
[JsonPropertyName("theme")]
public string Theme { get; set; }
[JsonPropertyName("brightness_update_rate")]
public string BrightnessUpdateRate { get; set; }
[JsonPropertyName("monitors")]
public List<MonitorInfo> Monitors { get; set; }
[JsonPropertyName("restore_settings_on_startup")]
public bool RestoreSettingsOnStartup { get; set; }
[JsonPropertyName("saved_monitor_settings")]
public Dictionary<string, MonitorSavedSettings> SavedMonitorSettings { get; set; }
[JsonPropertyName("enable_mcp_server")]
public bool EnableMcpServer { get; set; }
[JsonPropertyName("mcp_server_port")]
public int McpServerPort { get; set; }
[JsonPropertyName("mcp_auto_start")]
public bool McpAutoStart { get; set; }
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class PowerDisplaySettings : BasePTModuleSettings, ISettingsConfig
{
public const string ModuleName = "PowerDisplay";
[JsonPropertyName("properties")]
public PowerDisplayProperties Properties { get; set; }
public PowerDisplaySettings()
{
Properties = new PowerDisplayProperties();
Version = "1";
Name = ModuleName;
}
public string GetModuleName()
=> Name;
// This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration()
=> false;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -73,6 +73,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
case ModuleType.MeasureTool: return generalSettingsConfig.Enabled.MeasureTool;
case ModuleType.ShortcutGuide: return generalSettingsConfig.Enabled.ShortcutGuide;
case ModuleType.PowerOCR: return generalSettingsConfig.Enabled.PowerOcr;
case ModuleType.PowerDisplay: return generalSettingsConfig.Enabled.PowerDisplay;
case ModuleType.ZoomIt: return generalSettingsConfig.Enabled.ZoomIt;
default: return false;
}
@@ -109,6 +110,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
case ModuleType.MeasureTool: generalSettingsConfig.Enabled.MeasureTool = isEnabled; break;
case ModuleType.ShortcutGuide: generalSettingsConfig.Enabled.ShortcutGuide = isEnabled; break;
case ModuleType.PowerOCR: generalSettingsConfig.Enabled.PowerOcr = isEnabled; break;
case ModuleType.PowerDisplay: generalSettingsConfig.Enabled.PowerDisplay = isEnabled; break;
case ModuleType.ZoomIt: generalSettingsConfig.Enabled.ZoomIt = isEnabled; break;
}
}
@@ -144,6 +146,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
case ModuleType.MeasureTool: return GPOWrapper.GetConfiguredScreenRulerEnabledValue();
case ModuleType.ShortcutGuide: return GPOWrapper.GetConfiguredShortcutGuideEnabledValue();
case ModuleType.PowerOCR: return GPOWrapper.GetConfiguredTextExtractorEnabledValue();
case ModuleType.PowerDisplay: return GpoRuleConfigured.Unavailable;
case ModuleType.ZoomIt: return GPOWrapper.GetConfiguredZoomItEnabledValue();
default: return GpoRuleConfigured.Unavailable;
}
@@ -180,6 +183,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
ModuleType.MeasureTool => typeof(MeasureToolPage),
ModuleType.ShortcutGuide => typeof(ShortcutGuidePage),
ModuleType.PowerOCR => typeof(PowerOcrPage),
ModuleType.PowerDisplay => typeof(PowerDisplayPage),
ModuleType.ZoomIt => typeof(ZoomItPage),
_ => typeof(DashboardPage), // never called, all values listed above
};

View File

@@ -33,6 +33,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums
Workspaces,
WhatsNew,
RegistryPreview,
PowerDisplay,
NewPlus,
ZoomIt,
}

View File

@@ -32,6 +32,7 @@ namespace Microsoft.PowerToys.Settings.UI.SerializationContext;
[JsonSerializable(typeof(PowerLauncherSettings))]
[JsonSerializable(typeof(PowerOcrSettings))]
[JsonSerializable(typeof(PowerOcrSettings))]
[JsonSerializable(typeof(PowerDisplaySettings))]
[JsonSerializable(typeof(RegistryPreviewSettings))]
[JsonSerializable(typeof(ShortcutGuideSettings))]
[JsonSerializable(typeof(WINDOWPLACEMENT))]

View File

@@ -4,8 +4,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
using Microsoft.PowerToys.Settings.UI.Views;
using Windows.Data.Json;
@@ -19,6 +21,7 @@ namespace Microsoft.PowerToys.Settings.UI.Services
public static IPCResponseService Instance => _instance ??= new IPCResponseService();
public static event EventHandler<AllHotkeyConflictsEventArgs> AllHotkeyConflictsReceived;
public static event EventHandler<MonitorInfo[]> PowerDisplayMonitorsReceived;
public void RegisterForIPC()
{
@@ -47,6 +50,10 @@ namespace Microsoft.PowerToys.Settings.UI.Services
{
ProcessAllHotkeyConflicts(json);
}
else if (responseType.Equals("powerdisplay_monitors", StringComparison.Ordinal))
{
ProcessPowerDisplayMonitors(json);
}
}
}
catch (Exception)
@@ -195,5 +202,85 @@ namespace Microsoft.PowerToys.Settings.UI.Services
return conflictGroup;
}
private void ProcessPowerDisplayMonitors(JsonObject json)
{
try
{
var monitors = new List<MonitorInfo>();
if (json.TryGetValue("monitors", out IJsonValue monitorsValue) &&
monitorsValue.ValueType == JsonValueType.Array)
{
var monitorsArray = monitorsValue.GetArray();
foreach (var monitorItem in monitorsArray)
{
if (monitorItem.ValueType == JsonValueType.Object)
{
var monitorObj = monitorItem.GetObject();
string name = string.Empty;
string internalName = string.Empty;
string hardwareId = string.Empty;
string communicationMethod = string.Empty;
string monitorType = string.Empty;
int currentBrightness = 0;
int colorTemperature = 6500;
if (monitorObj.TryGetValue("name", out IJsonValue nameValue) &&
nameValue.ValueType == JsonValueType.String)
{
name = nameValue.GetString();
}
if (monitorObj.TryGetValue("internalName", out IJsonValue internalNameValue) &&
internalNameValue.ValueType == JsonValueType.String)
{
internalName = internalNameValue.GetString();
}
if (monitorObj.TryGetValue("hardwareId", out IJsonValue hardwareIdValue) &&
hardwareIdValue.ValueType == JsonValueType.String)
{
hardwareId = hardwareIdValue.GetString();
}
if (monitorObj.TryGetValue("communicationMethod", out IJsonValue communicationMethodValue) &&
communicationMethodValue.ValueType == JsonValueType.String)
{
communicationMethod = communicationMethodValue.GetString();
}
if (monitorObj.TryGetValue("monitorType", out IJsonValue monitorTypeValue) &&
monitorTypeValue.ValueType == JsonValueType.String)
{
monitorType = monitorTypeValue.GetString();
}
if (monitorObj.TryGetValue("currentBrightness", out IJsonValue currentBrightnessValue) &&
currentBrightnessValue.ValueType == JsonValueType.Number)
{
currentBrightness = (int)currentBrightnessValue.GetNumber();
}
if (monitorObj.TryGetValue("colorTemperature", out IJsonValue colorTemperatureValue) &&
colorTemperatureValue.ValueType == JsonValueType.Number)
{
colorTemperature = (int)colorTemperatureValue.GetNumber();
}
var monitorInfo = new MonitorInfo(name, internalName, hardwareId, communicationMethod, monitorType, currentBrightness, colorTemperature);
monitors.Add(monitorInfo);
}
}
}
PowerDisplayMonitorsReceived?.Invoke(this, monitors.ToArray());
}
catch (Exception)
{
// Ignore JSON parsing errors
}
}
}
}

View File

@@ -0,0 +1,36 @@
<Page
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.OobePowerDisplay"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<controls:OOBEPageControl x:Uid="Oobe_PowerDisplay" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/PowerDisplay.png">
<controls:OOBEPageControl.PageContent>
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock x:Uid="Oobe_HowToUse" Style="{ThemeResource OobeSubtitleStyle}" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_PowerDisplay_HowToUse" Background="Transparent" />
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
<tk7controls:MarkdownTextBlock x:Uid="Oobe_PowerDisplay_TipsAndTricks" Background="Transparent" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button
x:Uid="Launch_PowerDisplay"
Click="Launch_PowerDisplay_Click"
Style="{StaticResource AccentButtonStyle}" />
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />
<HyperlinkButton NavigateUri="https://aka.ms/PowerToysOverview_PowerDisplay" Style="{StaticResource TextButtonStyle}">
<TextBlock x:Uid="LearnMore_PowerDisplay" TextWrapping="Wrap" />
</HyperlinkButton>
</StackPanel>
</StackPanel>
</controls:OOBEPageControl.PageContent>
</controls:OOBEPageControl>
</Page>

View File

@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class OobePowerDisplay : Page
{
public OobePowerToysModule ViewModel { get; set; }
public OobePowerDisplay()
{
this.InitializeComponent();
ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.PowerDisplay]);
DataContext = ViewModel;
}
private void Launch_PowerDisplay_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
ShellPage.SendDefaultIPCMessage("{\"action\":{\"PowerDisplay\":{\"action_name\":\"Launch\", \"value\":\"\"}}}");
}
private void SettingsLaunchButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
if (OobeShellPage.OpenMainWindowCallback != null)
{
OobeShellPage.OpenMainWindowCallback(typeof(PowerDisplayPage));
}
ViewModel.LogOpeningSettingsEvent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
ViewModel.LogOpeningModuleEvent();
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
ViewModel.LogClosingModuleEvent();
}
}
}

View File

@@ -0,0 +1,164 @@
<local:NavigablePage
x:Class="Microsoft.PowerToys.Settings.UI.Views.PowerDisplayPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:library="using:Microsoft.PowerToys.Settings.UI.Library"
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
AutomationProperties.LandmarkType="Main"
mc:Ignorable="d">
<controls:SettingsPageControl x:Uid="PowerDisplay" ModuleImageSource="ms-appx:///Assets/Settings/Modules/PowerDisplay.png">
<controls:SettingsPageControl.ModuleContent>
<StackPanel ChildrenTransitions="{StaticResource SettingsCardsAnimations}" Orientation="Vertical">
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_Enable_PowerDisplay"
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/PowerDisplay.png}">
<ToggleSwitch IsOn="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</tkcontrols:SettingsCard>
<controls:SettingsGroup x:Uid="PowerDisplay_Configuration_GroupSettings" IsEnabled="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_LaunchButtonControl"
ActionIcon="{ui:FontIcon Glyph=&#xE8A7;}"
Command="{x:Bind ViewModel.LaunchEventHandler}"
HeaderIcon="{ui:FontIcon Glyph=&#xE770;}"
IsClickEnabled="True" />
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_LaunchAtStartup"
HeaderIcon="{ui:FontIcon Glyph=&#xE8B5;}">
<ToggleSwitch x:Uid="PowerDisplay_LaunchAtStartup_ToggleSwitch" IsOn="{x:Bind ViewModel.IsLaunchAtStartupEnabled, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_Theme"
HeaderIcon="{ui:FontIcon Glyph=&#xE706;}">
<ComboBox
ItemsSource="{x:Bind ViewModel.ThemeOptions}"
SelectedItem="{x:Bind ViewModel.Theme, Mode=TwoWay}"
MinWidth="120" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
Header="Restore saved values on startup"
Description="When enabled, PowerDisplay will restore the last saved brightness, contrast, color temperature and volume values on startup. When disabled, it will read current monitor values."
HeaderIcon="{ui:FontIcon Glyph=&#xE7B8;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.RestoreSettingsOnStartup, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_BrightnessUpdateRate"
HeaderIcon="{ui:FontIcon Glyph=&#xE916;}">
<ComboBox
ItemsSource="{x:Bind ViewModel.BrightnessUpdateRateOptions}"
SelectedItem="{x:Bind ViewModel.BrightnessUpdateRate, Mode=TwoWay}"
MinWidth="120" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="PowerDisplay_MCP_GroupSettings" IsEnabled="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_EnableMcpServer"
HeaderIcon="{ui:FontIcon Glyph=&#xE968;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.EnableMcpServer, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_McpServerPort"
HeaderIcon="{ui:FontIcon Glyph=&#xE8B7;}"
IsEnabled="{x:Bind ViewModel.EnableMcpServer, Mode=OneWay}">
<NumberBox
Value="{x:Bind ViewModel.McpServerPort, Mode=TwoWay}"
Minimum="1000"
Maximum="65535"
MinWidth="120" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_McpAutoStart"
HeaderIcon="{ui:FontIcon Glyph=&#xE8B5;}"
IsEnabled="{x:Bind ViewModel.EnableMcpServer, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.McpAutoStart, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="PowerDisplay_Monitors" IsEnabled="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=OneWay}">
<!-- 空状态提示 -->
<InfoBar
x:Uid="PowerDisplay_NoMonitorsDetected"
IsClosable="False"
IsOpen="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=OneWay}"
Visibility="{x:Bind ViewModel.HasMonitors, Mode=OneWay, Converter={StaticResource ReverseBoolToVisibilityConverter}}"
Severity="Informational">
<InfoBar.IconSource>
<FontIconSource FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="&#xE7F4;" />
</InfoBar.IconSource>
</InfoBar>
<!-- 显示器列表 -->
<ItemsControl
x:Name="MonitorsList"
HorizontalAlignment="Stretch"
Visibility="{x:Bind ViewModel.HasMonitors, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"
ItemsSource="{x:Bind ViewModel.Monitors, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="library:MonitorInfo">
<tkcontrols:SettingsExpander
Margin="0,0,0,2"
Header="{x:Bind Name}"
HeaderIcon="{ui:FontIcon Glyph=&#xE7F4;}"
IsExpanded="False">
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_Name">
<TextBlock Text="{x:Bind Name, Mode=OneWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_InternalName">
<TextBlock Text="{x:Bind InternalName, Mode=OneWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_Type">
<TextBlock Text="{x:Bind MonitorType, Mode=OneWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_CommunicationMethod">
<TextBlock Text="{x:Bind CommunicationMethod, Mode=OneWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_Brightness">
<TextBlock Text="{x:Bind CurrentBrightness, Mode=OneWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_ColorTemperature">
<TextBlock Text="{x:Bind ColorTemperature, Mode=OneWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_Reset">
<Button x:Uid="PowerDisplay_Monitor_ResetButton" Click="ResetButton_Click" Tag="{x:Bind}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_EnableColorTemperature">
<ToggleSwitch x:Uid="PowerDisplay_Monitor_EnableColorTemperature_ToggleSwitch" IsOn="{x:Bind EnableColorTemperature, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_EnableContrast">
<ToggleSwitch x:Uid="PowerDisplay_Monitor_EnableContrast_ToggleSwitch" IsOn="{x:Bind EnableContrast, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_EnableVolume">
<ToggleSwitch x:Uid="PowerDisplay_Monitor_EnableVolume_ToggleSwitch" IsOn="{x:Bind EnableVolume, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_HideMonitor">
<ToggleSwitch x:Uid="PowerDisplay_Monitor_HideMonitor_ToggleSwitch" IsOn="{x:Bind IsHidden, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</controls:SettingsGroup>
</StackPanel>
</controls:SettingsPageControl.ModuleContent>
<controls:SettingsPageControl.PrimaryLinks>
<controls:PageLink x:Uid="LearnMore_PowerDisplay" Link="https://aka.ms/PowerToysOverview_PowerDisplay" />
</controls:SettingsPageControl.PrimaryLinks>
</controls:SettingsPageControl>
</local:NavigablePage>

View File

@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using CommunityToolkit.WinUI.Controls;
using Microsoft.UI.Xaml;
using System.Windows.Controls;
namespace Microsoft.PowerToys.Settings.UI.Views
{
public sealed partial class PowerDisplayPage : NavigablePage, IRefreshablePage
{
private PowerDisplayViewModel ViewModel { get; set; }
public PowerDisplayPage()
{
var settingsUtils = new SettingsUtils();
ViewModel = new PowerDisplayViewModel(
SettingsRepository<GeneralSettings>.GetInstance(settingsUtils),
SettingsRepository<PowerDisplaySettings>.GetInstance(settingsUtils),
ShellPage.SendDefaultIPCMessage);
DataContext = ViewModel;
InitializeComponent();
}
public void RefreshEnabledState()
{
ViewModel.RefreshEnabledState();
}
private void ResetButton_Click(object sender, RoutedEventArgs e)
{
if (sender is Button button && button.Tag is MonitorInfo monitor)
{
ViewModel.ResetMonitorSettings(monitor);
}
}
}
}

View File

@@ -202,6 +202,12 @@
helpers:NavHelper.NavigateTo="views:ColorPickerPage"
AutomationProperties.AutomationId="ColorPickerNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/ColorPicker.png}" />
<NavigationViewItem
x:Name="PowerDisplayNavigationItem"
x:Uid="Shell_PowerDisplay"
helpers:NavHelper.NavigateTo="views:PowerDisplayPage"
AutomationProperties.AutomationId="PowerDisplayNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/PowerDisplay.png}" />
<NavigationViewItem
x:Name="PowerLauncherNavigationItem"
x:Uid="Shell_PowerLauncher"

View File

@@ -555,6 +555,10 @@ opera.exe</value>
<value>Color Picker</value>
<comment>Product name: Navigation view item name for Color Picker</comment>
</data>
<data name="Shell_PowerDisplay.Content" xml:space="preserve">
<value>Power Display</value>
<comment>Product name: Navigation view item name for Power Display</comment>
</data>
<data name="Shell_KeyboardManager.Content" xml:space="preserve">
<value>Keyboard Manager</value>
<comment>Product name: Navigation view item name for Keyboard Manager</comment>
@@ -5299,4 +5303,163 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<data name="UtilitiesHeader.Title" xml:space="preserve">
<value>Utilities</value>
</data>
<data name="PowerDisplay.ModuleTitle" xml:space="preserve">
<value>Power Display</value>
</data>
<data name="PowerDisplay.ModuleDescription" xml:space="preserve">
<value>A display management utility for brightness and power control</value>
</data>
<data name="PowerDisplay_Enable_PowerDisplay.Header" xml:space="preserve">
<value>Enable Power Display</value>
</data>
<data name="PowerDisplay_Configuration_GroupSettings.Header" xml:space="preserve">
<value>Configuration</value>
</data>
<data name="PowerDisplay_LaunchButtonControl.Header" xml:space="preserve">
<value>Open Power Display</value>
</data>
<data name="PowerDisplay_LaunchButtonControl.Description" xml:space="preserve">
<value>Launch the Power Display utility</value>
</data>
<data name="PowerDisplay_LaunchAtStartup.Header" xml:space="preserve">
<value>Launch at startup</value>
</data>
<data name="PowerDisplay_LaunchAtStartup.Description" xml:space="preserve">
<value>Automatically start Power Display when Windows starts</value>
</data>
<data name="PowerDisplay_LaunchAtStartup_ToggleSwitch.OnContent" xml:space="preserve">
<value>On</value>
</data>
<data name="PowerDisplay_LaunchAtStartup_ToggleSwitch.OffContent" xml:space="preserve">
<value>Off</value>
</data>
<data name="PowerDisplay_Theme.Header" xml:space="preserve">
<value>Theme</value>
</data>
<data name="PowerDisplay_Theme.Description" xml:space="preserve">
<value>Select theme for Power Display</value>
</data>
<data name="PowerDisplay_BrightnessUpdateRate.Header" xml:space="preserve">
<value>Brightness update rate</value>
</data>
<data name="PowerDisplay_BrightnessUpdateRate.Description" xml:space="preserve">
<value>How frequently to update brightness values</value>
</data>
<data name="PowerDisplay_Monitor_HideMonitor.Header" xml:space="preserve">
<value>Hide monitor</value>
</data>
<data name="PowerDisplay_Monitor_HideMonitor.Description" xml:space="preserve">
<value>Hide this monitor from the Power Display interface</value>
</data>
<data name="PowerDisplay_Monitor_HideMonitor_ToggleSwitch.OnContent" xml:space="preserve">
<value>On</value>
</data>
<data name="PowerDisplay_Monitor_HideMonitor_ToggleSwitch.OffContent" xml:space="preserve">
<value>Off</value>
</data>
<data name="PowerDisplay_Monitors.Header" xml:space="preserve">
<value>Monitors</value>
</data>
<data name="PowerDisplay_NoMonitorsDetected.Title" xml:space="preserve">
<value>No monitors detected</value>
</data>
<data name="PowerDisplay_NoMonitorsDetected.Message" xml:space="preserve">
<value>Please ensure your external monitors are connected and powered on.</value>
</data>
<data name="PowerDisplay_Monitor_Name.Header" xml:space="preserve">
<value>Display name</value>
</data>
<data name="PowerDisplay_Monitor_InternalName.Header" xml:space="preserve">
<value>Internal name</value>
</data>
<data name="PowerDisplay_Monitor_Type.Header" xml:space="preserve">
<value>Monitor type</value>
</data>
<data name="PowerDisplay_Monitor_CommunicationMethod.Header" xml:space="preserve">
<value>Communication method</value>
</data>
<data name="LearnMore_PowerDisplay.Text" xml:space="preserve">
<value>Learn more about Power Display</value>
</data>
<data name="Oobe_PowerDisplay.Title" xml:space="preserve">
<value>Power Display</value>
</data>
<data name="Oobe_PowerDisplay.Description" xml:space="preserve">
<value>Power Display provides advanced display management features including brightness control, auto-brightness, and power management settings.</value>
</data>
<data name="Launch_PowerDisplay.Content" xml:space="preserve">
<value>Open Power Display</value>
</data>
<data name="PowerDisplay_Monitor_Brightness.Header" xml:space="preserve">
<value>Brightness</value>
</data>
<data name="PowerDisplay_Monitor_ColorTemperature.Header" xml:space="preserve">
<value>Color Temperature</value>
</data>
<data name="PowerDisplay_Monitor_Reset.Header" xml:space="preserve">
<value>Reset Settings</value>
</data>
<data name="PowerDisplay_Monitor_Reset.Description" xml:space="preserve">
<value>Reset brightness and color temperature to default values</value>
</data>
<data name="PowerDisplay_Monitor_ResetButton.Content" xml:space="preserve">
<value>Reset</value>
</data>
<data name="PowerDisplay_Monitor_EnableColorTemperature.Header" xml:space="preserve">
<value>Enable color temperature</value>
</data>
<data name="PowerDisplay_Monitor_EnableColorTemperature.Description" xml:space="preserve">
<value>Show color temperature control for this monitor</value>
</data>
<data name="PowerDisplay_Monitor_EnableColorTemperature_ToggleSwitch.OnContent" xml:space="preserve">
<value>On</value>
</data>
<data name="PowerDisplay_Monitor_EnableColorTemperature_ToggleSwitch.OffContent" xml:space="preserve">
<value>Off</value>
</data>
<data name="PowerDisplay_Monitor_EnableContrast.Header" xml:space="preserve">
<value>Enable contrast</value>
</data>
<data name="PowerDisplay_Monitor_EnableContrast.Description" xml:space="preserve">
<value>Show contrast control for this monitor</value>
</data>
<data name="PowerDisplay_Monitor_EnableContrast_ToggleSwitch.OnContent" xml:space="preserve">
<value>On</value>
</data>
<data name="PowerDisplay_Monitor_EnableContrast_ToggleSwitch.OffContent" xml:space="preserve">
<value>Off</value>
</data>
<data name="PowerDisplay_Monitor_EnableVolume.Header" xml:space="preserve">
<value>Enable volume</value>
</data>
<data name="PowerDisplay_Monitor_EnableVolume.Description" xml:space="preserve">
<value>Show volume control for this monitor</value>
</data>
<data name="PowerDisplay_Monitor_EnableVolume_ToggleSwitch.OnContent" xml:space="preserve">
<value>On</value>
</data>
<data name="PowerDisplay_Monitor_EnableVolume_ToggleSwitch.OffContent" xml:space="preserve">
<value>Off</value>
</data>
<data name="PowerDisplay_MCP_GroupSettings.Header" xml:space="preserve">
<value>Model Context Protocol (MCP)</value>
</data>
<data name="PowerDisplay_EnableMcpServer.Header" xml:space="preserve">
<value>Enable MCP Server</value>
</data>
<data name="PowerDisplay_EnableMcpServer.Description" xml:space="preserve">
<value>Allow MCP clients like Claude Desktop to control monitor settings through standardized protocol</value>
</data>
<data name="PowerDisplay_McpServerPort.Header" xml:space="preserve">
<value>MCP Server Port</value>
</data>
<data name="PowerDisplay_McpServerPort.Description" xml:space="preserve">
<value>Port number for the MCP server to listen on (default: 5000)</value>
</data>
<data name="PowerDisplay_McpAutoStart.Header" xml:space="preserve">
<value>MCP Auto Start</value>
</data>
<data name="PowerDisplay_McpAutoStart.Description" xml:space="preserve">
<value>Automatically start MCP server when PowerDisplay launches</value>
</data>
</root>

View File

@@ -0,0 +1,594 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using System.Windows;
using global::PowerToys.GPOWrapper;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands;
using Microsoft.PowerToys.Settings.UI.SerializationContext;
using Microsoft.PowerToys.Settings.UI.Services;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
public partial class PowerDisplayViewModel : Observable
{
private GeneralSettings GeneralSettingsConfig { get; set; }
private ISettingsRepository<PowerDisplaySettings> _powerDisplayRepository;
private FileSystemWatcher _settingsWatcher;
public ButtonClickCommand LaunchEventHandler => new ButtonClickCommand(Launch);
public ButtonClickCommand GetResetMonitorCommand(MonitorInfo monitor) => new ButtonClickCommand(() => ResetMonitorSettings(monitor));
public PowerDisplayViewModel(ISettingsRepository<GeneralSettings> settingsRepository, ISettingsRepository<PowerDisplaySettings> powerDisplaySettingsRepository, Func<string, int> ipcMSGCallBackFunc)
{
// To obtain the general settings configurations of PowerToys Settings.
ArgumentNullException.ThrowIfNull(settingsRepository);
GeneralSettingsConfig = settingsRepository.SettingsConfig;
_powerDisplayRepository = powerDisplaySettingsRepository;
_settings = powerDisplaySettingsRepository.SettingsConfig;
InitializeEnabledValue();
// Initialize monitors collection
_monitors = new ObservableCollection<MonitorInfo>(_settings.Properties.Monitors);
_hasMonitors = _monitors.Count > 0;
// Subscribe to PropertyChanged events for existing monitors
SubscribeToAllMonitorChanges();
// set the callback functions value to handle outgoing IPC message.
SendConfigMSG = ipcMSGCallBackFunc;
// Subscribe to monitor information updates
IPCResponseService.PowerDisplayMonitorsReceived += OnMonitorsReceived;
// Don't setup settings file watcher for PowerDisplay's settings.json
// as it creates circular dependencies and the two apps use different formats
// SetupSettingsWatcher();
}
private void InitializeEnabledValue()
{
_isPowerDisplayEnabled = GeneralSettingsConfig.Enabled.PowerDisplay;
}
public bool IsPowerDisplayEnabled
{
get => _isPowerDisplayEnabled;
set
{
if (_isPowerDisplayEnabled != value)
{
_isPowerDisplayEnabled = value;
OnPropertyChanged(nameof(IsPowerDisplayEnabled));
GeneralSettingsConfig.Enabled.PowerDisplay = value;
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
SendConfigMSG(outgoing.ToString());
}
}
}
public bool IsLaunchAtStartupEnabled
{
get => _settings.Properties.LaunchAtStartup;
set
{
if (_settings.Properties.LaunchAtStartup != value)
{
_settings.Properties.LaunchAtStartup = value;
OnPropertyChanged(nameof(IsLaunchAtStartupEnabled));
NotifySettingsChanged();
}
}
}
public bool RestoreSettingsOnStartup
{
get => _settings.Properties.RestoreSettingsOnStartup;
set
{
if (_settings.Properties.RestoreSettingsOnStartup != value)
{
_settings.Properties.RestoreSettingsOnStartup = value;
OnPropertyChanged(nameof(RestoreSettingsOnStartup));
NotifySettingsChanged();
}
}
}
public string Theme
{
get => _settings.Properties.Theme;
set
{
if (_settings.Properties.Theme != value)
{
_settings.Properties.Theme = value;
OnPropertyChanged(nameof(Theme));
NotifySettingsChanged();
}
}
}
public string BrightnessUpdateRate
{
get => _settings.Properties.BrightnessUpdateRate;
set
{
if (_settings.Properties.BrightnessUpdateRate != value)
{
_settings.Properties.BrightnessUpdateRate = value;
OnPropertyChanged(nameof(BrightnessUpdateRate));
NotifySettingsChanged();
}
}
}
public bool EnableMcpServer
{
get => _settings.Properties.EnableMcpServer;
set
{
if (_settings.Properties.EnableMcpServer != value)
{
_settings.Properties.EnableMcpServer = value;
OnPropertyChanged(nameof(EnableMcpServer));
NotifySettingsChanged();
}
}
}
public int McpServerPort
{
get => _settings.Properties.McpServerPort;
set
{
if (_settings.Properties.McpServerPort != value)
{
_settings.Properties.McpServerPort = value;
OnPropertyChanged(nameof(McpServerPort));
NotifySettingsChanged();
}
}
}
public bool McpAutoStart
{
get => _settings.Properties.McpAutoStart;
set
{
if (_settings.Properties.McpAutoStart != value)
{
_settings.Properties.McpAutoStart = value;
OnPropertyChanged(nameof(McpAutoStart));
NotifySettingsChanged();
}
}
}
private readonly List<string> _brightnessUpdateRateOptions = new List<string>
{
"never",
"250ms",
"500ms",
"1s",
"2s"
};
public List<string> BrightnessUpdateRateOptions => _brightnessUpdateRateOptions;
private readonly List<string> _themeOptions = new List<string>
{
"Light",
"Dark"
};
public List<string> ThemeOptions => _themeOptions;
public ObservableCollection<MonitorInfo> Monitors
{
get => _monitors;
set
{
if (_monitors != value)
{
// Unsubscribe from old collection
if (_monitors != null)
{
UnsubscribeFromAllMonitorChanges();
}
_monitors = value;
// Subscribe to new collection
if (_monitors != null)
{
SubscribeToAllMonitorChanges();
}
OnPropertyChanged();
}
}
}
public bool HasMonitors
{
get => _hasMonitors;
set
{
if (_hasMonitors != value)
{
_hasMonitors = value;
OnPropertyChanged();
}
}
}
private void OnMonitorsReceived(object sender, MonitorInfo[] monitors)
{
UpdateMonitors(monitors);
}
public void UpdateMonitors(MonitorInfo[] monitors)
{
_isUpdatingSettings = true;
try
{
if (monitors == null)
{
// Unsubscribe from all existing monitors
UnsubscribeFromAllMonitorChanges();
_monitors.Clear();
HasMonitors = false;
_settings.Properties.Monitors = new List<MonitorInfo>();
NotifySettingsChanged();
return;
}
// Unsubscribe from all existing monitors
UnsubscribeFromAllMonitorChanges();
// Create a lookup of existing monitors to preserve user settings
var existingMonitors = _monitors.ToDictionary(m => GetMonitorKey(m), m => m);
_monitors.Clear();
foreach (var newMonitor in monitors)
{
var monitorKey = GetMonitorKey(newMonitor);
// Check if we have an existing monitor with the same key
if (existingMonitors.TryGetValue(monitorKey, out var existingMonitor))
{
// Preserve user settings from existing monitor
newMonitor.EnableColorTemperature = existingMonitor.EnableColorTemperature;
newMonitor.EnableContrast = existingMonitor.EnableContrast;
newMonitor.EnableVolume = existingMonitor.EnableVolume;
newMonitor.IsHidden = existingMonitor.IsHidden;
}
_monitors.Add(newMonitor);
// Subscribe to PropertyChanged for the new monitor
SubscribeToMonitorChanges(newMonitor);
}
// Update HasMonitors property
HasMonitors = monitors.Length > 0;
// Update settings
_settings.Properties.Monitors = _monitors.ToList();
NotifySettingsChanged();
}
finally
{
_isUpdatingSettings = false;
}
}
/// <summary>
/// Generate a unique key for monitor matching based on hardware ID and internal name
/// </summary>
private string GetMonitorKey(MonitorInfo monitor)
{
// Use hardware ID if available, otherwise fall back to internal name
if (!string.IsNullOrEmpty(monitor.HardwareId))
{
return monitor.HardwareId;
}
return monitor.InternalName ?? monitor.Name ?? string.Empty;
}
public void Dispose()
{
// Unsubscribe from monitor property changes
UnsubscribeFromAllMonitorChanges();
// Unsubscribe from events
IPCResponseService.PowerDisplayMonitorsReceived -= OnMonitorsReceived;
// Clean up settings file watcher
if (_settingsWatcher != null)
{
_settingsWatcher.Dispose();
}
}
/// <summary>
/// Subscribe to PropertyChanged events for all monitors in the collection
/// </summary>
private void SubscribeToAllMonitorChanges()
{
foreach (var monitor in _monitors)
{
monitor.PropertyChanged += OnMonitorPropertyChanged;
}
}
/// <summary>
/// Unsubscribe from PropertyChanged events for all monitors in the collection
/// </summary>
private void UnsubscribeFromAllMonitorChanges()
{
foreach (var monitor in _monitors)
{
monitor.PropertyChanged -= OnMonitorPropertyChanged;
}
}
/// <summary>
/// Subscribe to PropertyChanged event for a specific monitor
/// </summary>
private void SubscribeToMonitorChanges(MonitorInfo monitor)
{
monitor.PropertyChanged += OnMonitorPropertyChanged;
}
/// <summary>
/// Unsubscribe from PropertyChanged event for a specific monitor
/// </summary>
private void UnsubscribeFromMonitorChanges(MonitorInfo monitor)
{
monitor.PropertyChanged -= OnMonitorPropertyChanged;
}
private bool _isUpdatingSettings = false;
/// <summary>
/// Handle PropertyChanged events from MonitorInfo objects
/// </summary>
private void OnMonitorPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
// Prevent infinite loops during settings updates
if (_isUpdatingSettings) return;
if (sender is MonitorInfo monitor)
{
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Monitor {monitor.Name} property {e.PropertyName} changed to: EnableColorTemp={monitor.EnableColorTemperature}, EnableContrast={monitor.EnableContrast}, EnableVolume={monitor.EnableVolume}");
}
// Update the settings object to keep it in sync
_settings.Properties.Monitors = _monitors.ToList();
// Save settings when any monitor property changes
NotifySettingsChanged();
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Monitor property changed: {e.PropertyName}");
}
public void Launch()
{
var actionName = "Launch";
SendConfigMSG("{\"action\":{\"PowerDisplay\":{\"action_name\":\"" + actionName + "\", \"value\":\"\"}}}");
}
public void ResetMonitorSettings(MonitorInfo monitor)
{
if (monitor == null) return;
try
{
// Reset monitor values to defaults
monitor.CurrentBrightness = 30;
monitor.ColorTemperature = 6500;
// Update the saved settings with default values
if (_settings.Properties.SavedMonitorSettings == null)
{
_settings.Properties.SavedMonitorSettings = new Dictionary<string, MonitorSavedSettings>();
}
_settings.Properties.SavedMonitorSettings[monitor.InternalName] = new MonitorSavedSettings
{
Brightness = 30,
ColorTemperature = 6500,
Contrast = 50,
Volume = 50,
LastUpdated = DateTime.Now
};
// Save settings - this will trigger PowerDisplay's file watcher to apply the reset values
NotifySettingsChanged();
}
catch (Exception ex)
{
// Handle error gracefully
System.Diagnostics.Debug.WriteLine($"Failed to reset monitor settings: {ex.Message}");
}
}
private Func<string, int> SendConfigMSG { get; }
private bool _isPowerDisplayEnabled;
private PowerDisplaySettings _settings;
private ObservableCollection<MonitorInfo> _monitors;
private bool _hasMonitors;
public void RefreshEnabledState()
{
InitializeEnabledValue();
OnPropertyChanged(nameof(IsPowerDisplayEnabled));
}
private void NotifySettingsChanged()
{
// Using InvariantCulture as this is an IPC message
SendConfigMSG(
string.Format(
CultureInfo.InvariantCulture,
"{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
PowerDisplaySettings.ModuleName,
JsonSerializer.Serialize(_settings, SourceGenerationContextContext.Default.PowerDisplaySettings)));
// Also save directly to PowerDisplay's settings file for immediate pickup
SaveToPowerDisplaySettingsFile();
}
/// <summary>
/// Save settings directly to PowerDisplay's settings.json file
/// </summary>
private void SaveToPowerDisplaySettingsFile()
{
try
{
var settingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Microsoft", "PowerToys", "PowerDisplay", "settings.json");
var directory = Path.GetDirectoryName(settingsPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var jsonString = JsonSerializer.Serialize(_settings, SourceGenerationContextContext.Default.PowerDisplaySettings);
// Use retry logic to handle file access conflicts
int retryCount = 3;
for (int i = 0; i < retryCount; i++)
{
try
{
File.WriteAllText(settingsPath, jsonString);
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Settings saved to PowerDisplay file: {settingsPath}");
break; // Success, exit retry loop
}
catch (IOException) when (i < retryCount - 1)
{
// File is locked, wait and retry
System.Threading.Thread.Sleep(100);
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Failed to save to PowerDisplay settings file: {ex.Message}");
}
}
/// <summary>
/// 设置设置文件监视器,当 PowerDisplay.exe 更新设置文件时自动刷新 UI
/// </summary>
private void SetupSettingsWatcher()
{
try
{
var settingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Microsoft", "PowerToys", "PowerDisplay", "settings.json");
var directory = Path.GetDirectoryName(settingsPath);
if (!string.IsNullOrEmpty(directory))
{
// 确保目录存在
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
_settingsWatcher = new FileSystemWatcher(directory)
{
Filter = "settings.json",
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime,
EnableRaisingEvents = true
};
_settingsWatcher.Changed += OnSettingsFileChanged;
_settingsWatcher.Created += OnSettingsFileChanged;
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Settings file watcher setup for: {settingsPath}");
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Failed to setup settings file watcher: {ex.Message}");
}
}
/// <summary>
/// 处理设置文件变化事件
/// </summary>
private void OnSettingsFileChanged(object sender, FileSystemEventArgs e)
{
try
{
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Settings file changed: {e.FullPath}");
// 添加延迟确保文件写入完成
Task.Delay(500).ContinueWith(_ =>
{
try
{
Application.Current?.Dispatcher?.Invoke(() =>
{
// 重新加载设置
if (_powerDisplayRepository.ReloadSettings())
{
var newSettings = _powerDisplayRepository.SettingsConfig;
// 更新监视器列表
UpdateMonitors(newSettings.Properties.Monitors.ToArray());
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Settings reloaded, monitor count: {newSettings.Properties.Monitors.Count}");
}
});
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Failed to reload settings: {ex.Message}");
}
});
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Error handling settings file change: {ex.Message}");
}
}
}
}