2
0

Add brightness/contrast sliders, input source switching, and 9-language localization

- Add VCP commands for brightness (10), contrast (12), input source (60)
- Fix UTF-16 encoding for monitor data parsing
- Add system tray app with monitor controls
- Add localization for en, es, fr, de, zh, ja, pt, it, hi
- Update to .NET 9.0
- Add LICENSE and README

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-03 22:11:35 -05:00
parent 6b656ead2d
commit 0352c6b755
34 changed files with 1640 additions and 158 deletions

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>CMM.Library</RootNamespace>
<AssemblyName>CMM.Library</AssemblyName>

View File

@@ -30,13 +30,13 @@ public static class CMMCommand
return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} D6 4");
}
private static async Task<string> GetMonitorValue(string monitorSN, int? reTry = 0)
private static async Task<string> GetMonitorValue(string monitorSN, string vcpCode = "D6", int? reTry = 0)
{
var value = string.Empty;
while (reTry <= 5)
{
var cmdFileName = Path.Combine(CMMTmpFolder, $"{Guid.NewGuid()}.bat");
var cmd = $"{CMMexe} /GetValue {monitorSN} D6\r\n" +
var cmd = $"{CMMexe} /GetValue {monitorSN} {vcpCode}\r\n" +
$"echo %errorlevel%";
File.WriteAllText(cmdFileName, cmd);
var values = await ConsoleHelper.ExecuteCommand(cmdFileName);
@@ -46,12 +46,112 @@ public static class CMMCommand
if (!string.IsNullOrEmpty(value) && value != "0") return value;
await Task.Delay(500);
await GetMonitorValue(monitorSN, reTry++);
await GetMonitorValue(monitorSN, vcpCode, reTry++);
};
return value;
}
#region Brightness (VCP Code 10)
public static Task SetBrightness(string monitorSN, int value)
{
return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} 10 {value}");
}
public static async Task<int?> GetBrightness(string monitorSN)
{
var value = await GetMonitorValue(monitorSN, "10");
return int.TryParse(value, out var result) ? result : null;
}
#endregion
#region Contrast (VCP Code 12)
public static Task SetContrast(string monitorSN, int value)
{
return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} 12 {value}");
}
public static async Task<int?> GetContrast(string monitorSN)
{
var value = await GetMonitorValue(monitorSN, "12");
return int.TryParse(value, out var result) ? result : null;
}
#endregion
#region Input Source (VCP Code 60)
public static Task SetInputSource(string monitorSN, int value)
{
return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} 60 {value}");
}
public static async Task<int?> GetInputSource(string monitorSN)
{
var value = await GetMonitorValue(monitorSN, "60");
return int.TryParse(value, out var result) ? result : null;
}
public static async Task<List<InputSourceOption>> GetInputSourceOptions(string monitorSN)
{
var options = new List<InputSourceOption>();
var savePath = Path.Combine(CMMTmpFolder, $"{monitorSN}_vcp.tmp");
await ConsoleHelper.CmdCommandAsync($"{CMMexe} /sjson {savePath} {monitorSN}");
if (!File.Exists(savePath)) return options;
var monitorModel = JsonHelper.JsonFormFile<IEnumerable<SMonitorModel>>(savePath);
var inputSourceVcp = monitorModel?.FirstOrDefault(m => m.VCPCode == "60");
if (inputSourceVcp?.PossibleValues != null)
{
var possibleValues = inputSourceVcp.PossibleValues
.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(v => int.TryParse(v.Trim(), out var val) ? val : (int?)null)
.Where(v => v.HasValue)
.Select(v => v.Value);
foreach (var value in possibleValues)
{
options.Add(new InputSourceOption(value, GetInputSourceName(value)));
}
}
return options;
}
private static string GetInputSourceName(int vcpValue)
{
return vcpValue switch
{
1 => "VGA-1",
2 => "VGA-2",
3 => "DVI-1",
4 => "DVI-2",
5 => "Composite-1",
6 => "Composite-2",
7 => "S-Video-1",
8 => "S-Video-2",
9 => "Tuner-1",
10 => "Tuner-2",
11 => "Tuner-3",
12 => "Component-1",
13 => "Component-2",
14 => "Component-3",
15 => "DisplayPort-1",
16 => "DisplayPort-2",
17 => "HDMI-1",
18 => "HDMI-2",
_ => $"Input-{vcpValue}"
};
}
#endregion
public static async Task<string> GetMonPowerStatus(string monitorSN)
{
var status = await GetMonitorValue(monitorSN);
@@ -120,6 +220,9 @@ public static class CMMCommand
}
}
private static readonly string DebugLog = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "MonitorParse.log");
/// <summary>
/// 取得螢幕清單
/// </summary>
@@ -129,55 +232,54 @@ public static class CMMCommand
if (!File.Exists(CMMsMonitors)) return monitors;
XMonitor mon = null;
string context;
foreach (var line in await File.ReadAllLinesAsync(CMMsMonitors))
// Try reading raw bytes to understand encoding
var rawBytes = await File.ReadAllBytesAsync(CMMsMonitors);
File.WriteAllText(DebugLog, $"File size: {rawBytes.Length} bytes\nFirst 20 bytes: {BitConverter.ToString(rawBytes.Take(20).ToArray())}\n\n");
// Try UTF-16 LE (common Windows Unicode)
var content = System.Text.Encoding.Unicode.GetString(rawBytes);
File.AppendAllText(DebugLog, $"Content preview:\n{content.Substring(0, Math.Min(500, content.Length))}\n\n");
XMonitor? mon = null;
foreach (var line in content.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
{
var sp = line.Split(":", StringSplitOptions.RemoveEmptyEntries);
try
{
if (sp.Length != 2 || string.IsNullOrEmpty(sp[1])) continue;
File.AppendAllText(DebugLog, $"LINE: [{line}]\n");
context = sp[1].Substring(2, sp[1].Length - 3);
}
catch
{
continue;
}
var colonIdx = line.IndexOf(':');
if (colonIdx < 0) continue;
if (sp[0].StartsWith("Monitor Device Name"))
{
mon = new XMonitor();
mon.MonitorDeviceName = context;
continue;
}
var key = line.Substring(0, colonIdx).Trim();
var val = line.Substring(colonIdx + 1).Trim().Trim('"');
if (sp[0].StartsWith("Monitor Name"))
{
mon.MonitorName = context;
continue;
}
File.AppendAllText(DebugLog, $" KEY=[{key}] VAL=[{val}]\n");
if (sp[0].StartsWith("Serial Number"))
if (key.Contains("Monitor Device Name"))
{
mon.SerialNumber = context;
continue;
mon = new XMonitor { MonitorDeviceName = val };
}
if (sp[0].StartsWith("Adapter Name"))
else if (mon != null && key.Contains("Monitor Name"))
{
mon.AdapterName = context;
continue;
mon.MonitorName = val;
}
if (sp[0].StartsWith("Monitor ID"))
else if (mon != null && key.Contains("Serial Number"))
{
mon.MonitorID = context;
monitors.Add(mon);
continue;
mon.SerialNumber = val;
}
else if (mon != null && key.Contains("Adapter Name"))
{
mon.AdapterName = val;
}
else if (mon != null && key.Contains("Monitor ID"))
{
mon.MonitorID = val;
File.AppendAllText(DebugLog, $" -> Adding monitor: Name=[{mon.MonitorName}] SN=[{mon.SerialNumber}]\n");
if (!string.IsNullOrEmpty(mon.SerialNumber))
monitors.Add(mon);
mon = null;
}
}
File.AppendAllText(DebugLog, $"\nTotal monitors with serial: {monitors.Count}\n");
return monitors;
}

View File

@@ -2,6 +2,11 @@
namespace CMM.Library.ViewModel
{
public record InputSourceOption(int Value, string Name)
{
public override string ToString() => Name;
}
public class XMonitorStatus : PropertyBase
{
public string VCP_Code