From 0352c6b75599dcec314af3970e1b8e1eb54f4396 Mon Sep 17 00:00:00 2001 From: Dave Friedel Date: Sat, 3 Jan 2026 22:11:35 -0500 Subject: [PATCH] Add brightness/contrast sliders, input source switching, and 9-language localization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .claude/settings.local.json | 18 + CLAUDE.md | 89 ++++ CMMModel/CMMModel.csproj | 2 +- CMMService/CMMService.csproj | 2 +- ControlMyMonitorManagement/App.xaml.cs | 10 +- ControlMyMonitorManagement/Control/MonCtrl.cs | 246 ++++++++- .../ControlMyMonitorManagement.csproj | 2 +- ControlMyMonitorManagement/MainWindow.xaml | 2 +- .../Style/MonCtrlStyle.xaml | 27 +- DellMonitorControl/App.xaml | 15 +- DellMonitorControl/App.xaml.cs | 40 +- DellMonitorControl/ControlPanel.xaml | 14 +- DellMonitorControl/ControlPanel.xaml.cs | 471 ++++++++++++++++-- DellMonitorControl/DellMonitorControl.csproj | 3 +- DellMonitorControl/MainWindow.xaml | 28 +- DellMonitorControl/MainWindow.xaml.cs | 165 +++++- LICENSE | 37 ++ Language/CulturesHelper.cs | 40 +- Language/Language.csproj | 31 +- Language/StringResources.de.xaml | 29 ++ Language/StringResources.en.xaml | 29 ++ Language/StringResources.es.xaml | 29 ++ Language/StringResources.fr.xaml | 29 ++ Language/StringResources.hi.xaml | 29 ++ Language/StringResources.it.xaml | 29 ++ Language/StringResources.ja.xaml | 29 ++ Language/StringResources.pt.xaml | 29 ++ Language/StringResources.xaml | 32 +- Language/StringResources.zh.xaml | 29 ++ Library/Library.csproj | 2 +- Library/Method/CMMCommand.cs | 180 +++++-- Library/ViewModel/XMonitorStatus.cs | 5 + README.md | 74 +++ Tester/Tester.csproj | 2 +- 34 files changed, 1640 insertions(+), 158 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 CLAUDE.md create mode 100644 LICENSE create mode 100644 Language/StringResources.de.xaml create mode 100644 Language/StringResources.en.xaml create mode 100644 Language/StringResources.es.xaml create mode 100644 Language/StringResources.fr.xaml create mode 100644 Language/StringResources.hi.xaml create mode 100644 Language/StringResources.it.xaml create mode 100644 Language/StringResources.ja.xaml create mode 100644 Language/StringResources.pt.xaml create mode 100644 Language/StringResources.zh.xaml create mode 100644 README.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..abf4b57 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,18 @@ +{ + "permissions": { + "allow": [ + "Bash(dir \"C:\\\\Users\\\\logik\\\\source\\\\repos\\\\ControlMyMonitorManagement\\\\Language\")", + "Bash(find:*)", + "Bash(dir \"C:\\\\Users\\\\logik\\\\source\\\\repos\\\\ControlMyMonitorManagement\\\\Language\" /S)", + "Bash(cat:*)", + "Bash(dotnet build:*)", + "Bash(dotnet --list-sdks:*)", + "Bash(dotnet:*)", + "Bash(timeout /t 5 /nobreak)", + "Bash(taskkill:*)", + "Bash(start \"\" \"C:\\\\Users\\\\logik\\\\source\\\\repos\\\\ControlMyMonitorManagement\\\\DellMonitorControl\\\\bin\\\\Debug\\\\net9.0-windows\\\\DellMonitorControl.exe\")", + "Bash(ping:*)", + "Bash(git remote:*)" + ] + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e473c4d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,89 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +ControlMyMonitorManagement is a C# WPF desktop application for controlling monitor settings via DDC/CI (Display Data Channel/Command Interface). It allows controlling VCP (Virtual Control Panel) codes like brightness, contrast, and power state across multiple displays. + +## Build Commands + +```bash +# Build entire solution +dotnet build ControlMyMonitorManagement.sln + +# Build specific project +dotnet build ControlMyMonitorManagement/ControlMyMonitorManagement.csproj +dotnet build DellMonitorControl/DellMonitorControl.csproj + +# Run tests +dotnet test Tester/Tester.csproj + +# Run main application +dotnet run --project ControlMyMonitorManagement/ControlMyMonitorManagement.csproj + +# Run Dell-specific application +dotnet run --project DellMonitorControl/DellMonitorControl.csproj +``` + +## Architecture + +### Solution Structure (7 Projects) + +``` +ControlMyMonitorManagement.sln +├── ControlMyMonitorManagement # Main WPF UI application +├── DellMonitorControl # Dell-specific WPF app with system tray +├── Library # Core business logic and monitor control +├── CMMModel # Data models for monitor status/VCP codes +├── CMMService # Service layer (Interface/, Service/) +├── Language # Localization support (CulturesHelper) +└── Tester # NUnit test project +``` + +### Key Components in Library Project + +- **Method/CMMMgr.cs** - Main manager class, initializes monitor collection +- **Method/CMMCommand.cs** - Core monitor control commands (PowerOn, Sleep, SetBrightness, SetContrast, SetInputSource, etc.) +- **ViewModel/XMonitor.cs** - Monitor data model (device name, adapter, serial number) +- **ViewModel/XMonitorStatus.cs** - VCP feature status (code, value, max) +- **WinAPI/Win32Api.cs** - Windows API interop for DDC/CI commands +- **Config/Config.cs** - JSON configuration management +- **Helpers/ConsoleHelper.cs** - Process wrapper for cmd.exe commands + +### VCP Codes Reference + +Common VCP codes used in this project: +- **D6** (Power): 1=on, 4=sleep, 5=off +- **10** (Brightness): 0-100 +- **12** (Contrast): 0-100 +- **60** (Input Source): 1=VGA, 3=DVI, 15=DisplayPort, 17=HDMI + +## Localization + +Supports 9 languages with auto-detection from system locale: +- English (en), Spanish (es), French (fr), German (de) +- Chinese (zh), Japanese (ja), Portuguese (pt), Italian (it), Hindi (hi) + +Resource files: `Language/StringResources.{locale}.xaml` +Helper: `Language/CulturesHelper.cs` - call `ApplySystemLanguage()` on startup + +### Embedded Resource + +`Library/Resource/ControlMyMonitor.exe` - Third-party tool (by Nir Sofer) embedded for DDC/CI operations + +## Patterns Used + +- **MVVM** - PropertyBase class implements INotifyPropertyChanged +- **Async/Await** - Monitor scanning operations are async +- **Dependency Injection** - Used in DellMonitorControl for configuration + +## Target Framework + +.NET 7.0-windows with WPF (UseWPF=true) + +## Versioning + +Dynamic version format: `Major.Minor.DaysSinceProjectStart.HourMinutes` +- Main app project start: 2017-09-17 +- DellMonitorControl start: 2023-07-02 diff --git a/CMMModel/CMMModel.csproj b/CMMModel/CMMModel.csproj index 0cbd1f4..d96abc3 100644 --- a/CMMModel/CMMModel.csproj +++ b/CMMModel/CMMModel.csproj @@ -1,7 +1,7 @@  - net7.0-windows + net9.0-windows CMM.Model CMM.Model ControlMyMonitorManagement diff --git a/CMMService/CMMService.csproj b/CMMService/CMMService.csproj index 134eab8..2b6a584 100644 --- a/CMMService/CMMService.csproj +++ b/CMMService/CMMService.csproj @@ -1,7 +1,7 @@ - net7.0-windows + net9.0-windows enable CMM.Service CMM.Service diff --git a/ControlMyMonitorManagement/App.xaml.cs b/ControlMyMonitorManagement/App.xaml.cs index bf7a075..3b22efc 100644 --- a/ControlMyMonitorManagement/App.xaml.cs +++ b/ControlMyMonitorManagement/App.xaml.cs @@ -1,10 +1,6 @@ -using CMM.Library.Config; +using CMM.Language; +using CMM.Library.Config; using CMM.Library.Method; -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Data; -using System.Linq; using System.Threading.Tasks; using System.Windows; @@ -17,6 +13,7 @@ namespace CMM.Management { internal static XConfig cfg { get; private set; } internal static CMMMgr CMMMgr { get; private set; } + private readonly CulturesHelper _culturesHelper = new(); public App() { @@ -28,6 +25,7 @@ namespace CMM.Management private async Task App_Startup(object sender, StartupEventArgs e) { + _culturesHelper.ApplySystemLanguage(); await CMMMgr.Init(); } } diff --git a/ControlMyMonitorManagement/Control/MonCtrl.cs b/ControlMyMonitorManagement/Control/MonCtrl.cs index d0b85c9..4f099bf 100644 --- a/ControlMyMonitorManagement/Control/MonCtrl.cs +++ b/ControlMyMonitorManagement/Control/MonCtrl.cs @@ -1,19 +1,20 @@ -using System.Windows.Controls; +using System.Windows.Controls; using System.Windows; using CMM.Library.ViewModel; -using CMM.Library.Base; using CMM.Library.Method; -using System.Windows.Data; +using CMM.Language; using System; +using System.Collections.Generic; +using System.Threading.Tasks; namespace CMM.Management.Control { /// - /// 單一顆螢幕 + /// Single monitor control /// internal class MonCtrl : System.Windows.Controls.Control { - public readonly static DependencyProperty MonProperty; + public static readonly DependencyProperty MonProperty; private StackPanel _sp; static MonCtrl() @@ -30,6 +31,10 @@ namespace CMM.Management.Control public override void OnApplyTemplate() { _sp = Template.FindName("sp", this) as StackPanel; + if (_sp != null && Mon != null) + { + _ = LoadControlsAsync(); + } } public XMonitor Mon @@ -49,7 +54,236 @@ namespace CMM.Management.Control public virtual void OnMonChanged(XMonitor value) { - + if (_sp != null && value != null) + { + _ = LoadControlsAsync(); + } + } + + private async Task LoadControlsAsync() + { + if (_sp == null || Mon == null) return; + + _sp.Children.Clear(); + + var brightness = await CMMCommand.GetBrightness(Mon.SerialNumber); + var contrast = await CMMCommand.GetContrast(Mon.SerialNumber); + var inputSource = await CMMCommand.GetInputSource(Mon.SerialNumber); + var inputOptions = await CMMCommand.GetInputSourceOptions(Mon.SerialNumber); + var powerStatus = await CMMCommand.GetMonPowerStatus(Mon.SerialNumber); + + // Brightness slider + var brightnessPanel = CreateSliderControl( + Lang.Find("Brightness"), + brightness ?? 50, + Mon.SerialNumber, + async (sn, value) => await CMMCommand.SetBrightness(sn, value)); + _sp.Children.Add(brightnessPanel); + + // Contrast slider + var contrastPanel = CreateSliderControl( + Lang.Find("Contrast"), + contrast ?? 50, + Mon.SerialNumber, + async (sn, value) => await CMMCommand.SetContrast(sn, value)); + _sp.Children.Add(contrastPanel); + + // Input source dropdown + if (inputOptions.Count > 0) + { + var inputPanel = CreateInputSourceControl( + Mon.SerialNumber, + inputSource, + inputOptions); + _sp.Children.Add(inputPanel); + } + + // Power button + var powerPanel = CreatePowerControl(Mon.SerialNumber, powerStatus); + _sp.Children.Add(powerPanel); + } + + private StackPanel CreateSliderControl( + string label, + int currentValue, + string monitorSN, + Func onValueChanged) + { + var panel = new StackPanel + { + Orientation = Orientation.Horizontal, + Margin = new Thickness(0, 5, 0, 5) + }; + + var labelBlock = new TextBlock + { + Text = label, + Width = 100, + VerticalAlignment = VerticalAlignment.Center, + Foreground = System.Windows.Media.Brushes.White + }; + + var slider = new Slider + { + Style = (Style)FindResource("Horizontal_Slider"), + Minimum = 0, + Maximum = 100, + Value = currentValue, + Tag = monitorSN, + Width = 200 + }; + + var valueBlock = new TextBlock + { + Text = currentValue.ToString(), + Width = 40, + VerticalAlignment = VerticalAlignment.Center, + Foreground = System.Windows.Media.Brushes.White, + TextAlignment = TextAlignment.Right + }; + + // Update value display while dragging + slider.ValueChanged += (s, e) => + { + valueBlock.Text = ((int)e.NewValue).ToString(); + }; + + // Only send command on release (PreviewMouseUp) + slider.PreviewMouseUp += async (s, e) => + { + var sld = s as Slider; + var sn = sld?.Tag?.ToString(); + if (!string.IsNullOrEmpty(sn)) + { + await onValueChanged(sn, (int)sld.Value); + } + }; + + panel.Children.Add(labelBlock); + panel.Children.Add(slider); + panel.Children.Add(valueBlock); + + return panel; + } + + private StackPanel CreateInputSourceControl( + string monitorSN, + int? currentInputSource, + List options) + { + var panel = new StackPanel + { + Orientation = Orientation.Horizontal, + Margin = new Thickness(0, 5, 0, 5) + }; + + var labelBlock = new TextBlock + { + Text = Lang.Find("InputSource"), + Width = 100, + VerticalAlignment = VerticalAlignment.Center, + Foreground = System.Windows.Media.Brushes.White + }; + + var comboBox = new ComboBox + { + Width = 200, + Tag = monitorSN, + ItemsSource = options, + DisplayMemberPath = "Name" + }; + + // Set current selection + if (currentInputSource.HasValue) + { + var currentOption = options.Find(o => o.Value == currentInputSource.Value); + if (currentOption != null) + { + comboBox.SelectedItem = currentOption; + } + } + + comboBox.SelectionChanged += async (s, e) => + { + var cb = s as ComboBox; + var sn = cb?.Tag?.ToString(); + var selectedOption = cb?.SelectedItem as InputSourceOption; + + if (!string.IsNullOrEmpty(sn) && selectedOption != null) + { + await CMMCommand.SetInputSource(sn, selectedOption.Value); + } + }; + + panel.Children.Add(labelBlock); + panel.Children.Add(comboBox); + + return panel; + } + + private StackPanel CreatePowerControl(string monitorSN, string powerStatus) + { + var panel = new StackPanel + { + Orientation = Orientation.Horizontal, + Margin = new Thickness(0, 10, 0, 0) + }; + + var labelBlock = new TextBlock + { + Text = Lang.Find("Power"), + Width = 100, + VerticalAlignment = VerticalAlignment.Center, + Foreground = System.Windows.Media.Brushes.White + }; + + var btn = new Button + { + Tag = monitorSN, + Content = GetLocalizedPowerStatus(powerStatus), + Width = 200, + Height = 30 + }; + + btn.Click += async (s, e) => await TogglePower(s, e); + + panel.Children.Add(labelBlock); + panel.Children.Add(btn); + + return panel; + } + + private string GetLocalizedPowerStatus(string status) + { + return status switch + { + "PowerOn" => Lang.Find("PowerOn"), + "Sleep" => Lang.Find("Sleep"), + "PowerOff" => Lang.Find("PowerOff"), + _ => status + }; + } + + private async Task TogglePower(object sender, RoutedEventArgs e) + { + var btn = sender as Button; + var tag = btn?.Tag?.ToString(); + if (string.IsNullOrEmpty(tag)) return; + + var currentStatus = await CMMCommand.GetMonPowerStatus(tag); + + if (currentStatus == "Sleep" || currentStatus == "PowerOff") + { + await CMMCommand.PowerOn(tag); + } + else + { + await CMMCommand.Sleep(tag); + } + + await Task.Delay(1000); + var newStatus = await CMMCommand.GetMonPowerStatus(tag); + btn!.Content = GetLocalizedPowerStatus(newStatus); } } } diff --git a/ControlMyMonitorManagement/ControlMyMonitorManagement.csproj b/ControlMyMonitorManagement/ControlMyMonitorManagement.csproj index 3062287..9f9b2ea 100644 --- a/ControlMyMonitorManagement/ControlMyMonitorManagement.csproj +++ b/ControlMyMonitorManagement/ControlMyMonitorManagement.csproj @@ -2,7 +2,7 @@ WinExe - net7.0-windows + net9.0-windows CMM.Management true Copyright © DangWang $([System.DateTime]::Now.ToString(yyyy)) diff --git a/ControlMyMonitorManagement/MainWindow.xaml b/ControlMyMonitorManagement/MainWindow.xaml index bb1e9d4..fe76669 100644 --- a/ControlMyMonitorManagement/MainWindow.xaml +++ b/ControlMyMonitorManagement/MainWindow.xaml @@ -6,7 +6,7 @@ xmlns:ctrl="clr-namespace:CMM.Management.Control" xmlns:local="clr-namespace:CMM.Management" mc:Ignorable="d" - Title="ControlMyMonitor Management" + Title="{DynamicResource AppTitle}" Background="#666C6161" AllowsTransparency="True" ResizeMode="CanResizeWithGrip" diff --git a/ControlMyMonitorManagement/Style/MonCtrlStyle.xaml b/ControlMyMonitorManagement/Style/MonCtrlStyle.xaml index debd0d9..36008d7 100644 --- a/ControlMyMonitorManagement/Style/MonCtrlStyle.xaml +++ b/ControlMyMonitorManagement/Style/MonCtrlStyle.xaml @@ -1,18 +1,33 @@ - - \ No newline at end of file + diff --git a/DellMonitorControl/App.xaml b/DellMonitorControl/App.xaml index aceae59..939a665 100644 --- a/DellMonitorControl/App.xaml +++ b/DellMonitorControl/App.xaml @@ -1,17 +1,12 @@  + Startup="Application_Startup"> - - - - - - - - - + diff --git a/DellMonitorControl/App.xaml.cs b/DellMonitorControl/App.xaml.cs index aa03a85..b5cad1b 100644 --- a/DellMonitorControl/App.xaml.cs +++ b/DellMonitorControl/App.xaml.cs @@ -1,17 +1,39 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Data; -using System.Linq; -using System.Threading.Tasks; +using Hardcodet.Wpf.TaskbarNotification; using System.Windows; namespace DellMonitorControl { - /// - /// Interaction logic for App.xaml - /// public partial class App : Application { + private TaskbarIcon? _trayIcon; + private MainWindow? _mainWindow; + + private void Application_Startup(object sender, StartupEventArgs e) + { + _trayIcon = (TaskbarIcon)FindResource("TrayIcon"); + _trayIcon.TrayLeftMouseUp += TrayIcon_Click; + + _mainWindow = new MainWindow(); + _mainWindow.Show(); + } + + private void TrayIcon_Click(object sender, RoutedEventArgs e) + { + if (_mainWindow == null) return; + + if (_mainWindow.IsVisible) + _mainWindow.Hide(); + else + { + _mainWindow.Show(); + _mainWindow.Activate(); + } + } + + protected override void OnExit(ExitEventArgs e) + { + _trayIcon?.Dispose(); + base.OnExit(e); + } } } diff --git a/DellMonitorControl/ControlPanel.xaml b/DellMonitorControl/ControlPanel.xaml index 6f786ef..e9c4317 100644 --- a/DellMonitorControl/ControlPanel.xaml +++ b/DellMonitorControl/ControlPanel.xaml @@ -1,14 +1,14 @@  - - + mc:Ignorable="d" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + Width="280" Height="300"> + - + + diff --git a/DellMonitorControl/ControlPanel.xaml.cs b/DellMonitorControl/ControlPanel.xaml.cs index 3bc86b7..033de76 100644 --- a/DellMonitorControl/ControlPanel.xaml.cs +++ b/DellMonitorControl/ControlPanel.xaml.cs @@ -1,5 +1,9 @@ -using CMM.Library.Method; +using CMM.Language; +using CMM.Library.Method; using CMM.Library.ViewModel; +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -19,59 +23,434 @@ public partial class ControlPanel : UserControl public async Task Refresh() { - await CMMCommand.ScanMonitor(); - var monitors = await CMMCommand.ReadMonitorsData(); - sp.Children.Clear(); - - foreach (var m in monitors) + try { - var status = await CMMCommand.GetMonPowerStatus(m.SerialNumber); - var ctrl = CreatControl(m, status); - sp.Children.Add(ctrl); + sp.Children.Clear(); + + await CMMCommand.ScanMonitor(); + var monitors = (await CMMCommand.ReadMonitorsData()).ToList(); + + if (!monitors.Any()) + { + sp.Children.Add(new TextBlock + { + Text = "No DDC/CI monitors detected", + Foreground = System.Windows.Media.Brushes.White, + FontSize = 14 + }); + return; + } + + foreach (var m in monitors) + { + var brightness = await CMMCommand.GetBrightness(m.SerialNumber) ?? 50; + var contrast = await CMMCommand.GetContrast(m.SerialNumber) ?? 50; + var inputSource = await CMMCommand.GetInputSource(m.SerialNumber); + var inputOptions = await CMMCommand.GetInputSourceOptions(m.SerialNumber); + var powerStatus = await CMMCommand.GetMonPowerStatus(m.SerialNumber) ?? "Unknown"; + + // Monitor name header + sp.Children.Add(new TextBlock + { + Text = m.MonitorName, + Foreground = System.Windows.Media.Brushes.White, + FontSize = 14, + FontWeight = FontWeights.Bold, + Margin = new Thickness(0, 10, 0, 5) + }); + + // Brightness row + var brightnessRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) }; + brightnessRow.Children.Add(new TextBlock { Text = "Brightness", Foreground = System.Windows.Media.Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center }); + var bSlider = new Slider { Minimum = 0, Maximum = 100, Value = brightness, Width = 140, VerticalAlignment = VerticalAlignment.Center, Tag = m.SerialNumber }; + var bValue = new TextBlock { Text = brightness.ToString(), Foreground = System.Windows.Media.Brushes.White, Width = 30, FontSize = 12, Margin = new Thickness(5, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center }; + bSlider.ValueChanged += (s, e) => bValue.Text = ((int)e.NewValue).ToString(); + bSlider.PreviewMouseUp += async (s, e) => { if (s is Slider sl && sl.Tag is string sn) await CMMCommand.SetBrightness(sn, (int)sl.Value); }; + brightnessRow.Children.Add(bSlider); + brightnessRow.Children.Add(bValue); + sp.Children.Add(brightnessRow); + + // Contrast row + var contrastRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) }; + contrastRow.Children.Add(new TextBlock { Text = "Contrast", Foreground = System.Windows.Media.Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center }); + var cSlider = new Slider { Minimum = 0, Maximum = 100, Value = contrast, Width = 140, VerticalAlignment = VerticalAlignment.Center, Tag = m.SerialNumber }; + var cValue = new TextBlock { Text = contrast.ToString(), Foreground = System.Windows.Media.Brushes.White, Width = 30, FontSize = 12, Margin = new Thickness(5, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center }; + cSlider.ValueChanged += (s, e) => cValue.Text = ((int)e.NewValue).ToString(); + cSlider.PreviewMouseUp += async (s, e) => { if (s is Slider sl && sl.Tag is string sn) await CMMCommand.SetContrast(sn, (int)sl.Value); }; + contrastRow.Children.Add(cSlider); + contrastRow.Children.Add(cValue); + sp.Children.Add(contrastRow); + + // Input source row (if options available) + if (inputOptions.Count > 0) + { + var inputRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) }; + inputRow.Children.Add(new TextBlock { Text = "Input", Foreground = System.Windows.Media.Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center }); + var combo = new ComboBox { Width = 170, ItemsSource = inputOptions, DisplayMemberPath = "Name", Tag = m.SerialNumber }; + if (inputSource.HasValue) combo.SelectedItem = inputOptions.Find(o => o.Value == inputSource.Value); + combo.SelectionChanged += async (s, e) => { if (s is ComboBox cb && cb.Tag is string sn && cb.SelectedItem is InputSourceOption opt) await CMMCommand.SetInputSource(sn, opt.Value); }; + inputRow.Children.Add(combo); + sp.Children.Add(inputRow); + } + + // Power row + var powerRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) }; + powerRow.Children.Add(new TextBlock { Text = "Power", Foreground = System.Windows.Media.Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center }); + var powerBtn = new Button { Content = powerStatus, Width = 170, Tag = m.SerialNumber }; + powerBtn.Click += async (s, e) => + { + if (s is Button btn && btn.Tag is string sn) + { + var status = await CMMCommand.GetMonPowerStatus(sn); + if (status == "Sleep" || status == "PowerOff") await CMMCommand.PowerOn(sn); + else await CMMCommand.Sleep(sn); + await Task.Delay(1000); + btn.Content = await CMMCommand.GetMonPowerStatus(sn); + } + }; + powerRow.Children.Add(powerBtn); + sp.Children.Add(powerRow); + } + } + catch (Exception ex) + { + sp.Children.Clear(); + sp.Children.Add(new TextBlock + { + Text = $"Error: {ex.Message}", + Foreground = System.Windows.Media.Brushes.Red, + FontSize = 12, + TextWrapping = TextWrapping.Wrap + }); } } - private StackPanel CreatControl(XMonitor monitorModel, string powerStatus) + private StackPanel CreateControlSimple( + XMonitor monitorModel, + int? brightness, + int? contrast, + int? currentInputSource, + List inputOptions, + string powerStatus) { - var _sp = new StackPanel(); - - _sp.Orientation = Orientation.Vertical; - _sp.Margin = new Thickness(10, 5, 5, 0); - - var tb = new TextBlock + var container = new StackPanel { - Text = $"{monitorModel.MonitorName}({monitorModel.SerialNumber})", + Orientation = Orientation.Vertical, + Margin = new Thickness(0, 5, 0, 10) + }; + + // Monitor name + container.Children.Add(new TextBlock + { + Text = $"{monitorModel.MonitorName} ({monitorModel.SerialNumber})", + Foreground = System.Windows.Media.Brushes.White, + FontSize = 14, + FontWeight = FontWeights.Bold, + Margin = new Thickness(0, 0, 0, 5) + }); + + // Brightness + var brightnessPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 2, 0, 2) }; + brightnessPanel.Children.Add(new TextBlock { Text = "Brightness:", Foreground = System.Windows.Media.Brushes.LightGray, Width = 80, FontSize = 12 }); + var brightnessSlider = new Slider { Minimum = 0, Maximum = 100, Value = brightness ?? 50, Width = 150, Tag = monitorModel.SerialNumber }; + var brightnessValue = new TextBlock { Text = (brightness ?? 50).ToString(), Foreground = System.Windows.Media.Brushes.White, Width = 30, FontSize = 12, Margin = new Thickness(5, 0, 0, 0) }; + brightnessSlider.ValueChanged += (s, e) => brightnessValue.Text = ((int)e.NewValue).ToString(); + brightnessSlider.PreviewMouseUp += async (s, e) => + { + var sld = s as Slider; + if (sld?.Tag is string sn) + await CMMCommand.SetBrightness(sn, (int)sld.Value); + }; + brightnessPanel.Children.Add(brightnessSlider); + brightnessPanel.Children.Add(brightnessValue); + container.Children.Add(brightnessPanel); + + // Contrast + var contrastPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 2, 0, 2) }; + contrastPanel.Children.Add(new TextBlock { Text = "Contrast:", Foreground = System.Windows.Media.Brushes.LightGray, Width = 80, FontSize = 12 }); + var contrastSlider = new Slider { Minimum = 0, Maximum = 100, Value = contrast ?? 50, Width = 150, Tag = monitorModel.SerialNumber }; + var contrastValue = new TextBlock { Text = (contrast ?? 50).ToString(), Foreground = System.Windows.Media.Brushes.White, Width = 30, FontSize = 12, Margin = new Thickness(5, 0, 0, 0) }; + contrastSlider.ValueChanged += (s, e) => contrastValue.Text = ((int)e.NewValue).ToString(); + contrastSlider.PreviewMouseUp += async (s, e) => + { + var sld = s as Slider; + if (sld?.Tag is string sn) + await CMMCommand.SetContrast(sn, (int)sld.Value); + }; + contrastPanel.Children.Add(contrastSlider); + contrastPanel.Children.Add(contrastValue); + container.Children.Add(contrastPanel); + + // Input source (if available) + if (inputOptions.Count > 0) + { + var inputPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 2, 0, 2) }; + inputPanel.Children.Add(new TextBlock { Text = "Input:", Foreground = System.Windows.Media.Brushes.LightGray, Width = 80, FontSize = 12 }); + var combo = new ComboBox { Width = 150, ItemsSource = inputOptions, DisplayMemberPath = "Name", Tag = monitorModel.SerialNumber }; + if (currentInputSource.HasValue) + combo.SelectedItem = inputOptions.Find(o => o.Value == currentInputSource.Value); + combo.SelectionChanged += async (s, e) => + { + var cb = s as ComboBox; + if (cb?.Tag is string sn && cb.SelectedItem is InputSourceOption opt) + await CMMCommand.SetInputSource(sn, opt.Value); + }; + inputPanel.Children.Add(combo); + container.Children.Add(inputPanel); + } + + // Power + var powerPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 2, 0, 2) }; + powerPanel.Children.Add(new TextBlock { Text = "Power:", Foreground = System.Windows.Media.Brushes.LightGray, Width = 80, FontSize = 12 }); + var powerBtn = new Button { Content = powerStatus, Width = 150, Tag = monitorModel.SerialNumber }; + powerBtn.Click += async (s, e) => + { + var btn = s as Button; + if (btn?.Tag is string sn) + { + var status = await CMMCommand.GetMonPowerStatus(sn); + if (status == "Sleep" || status == "PowerOff") + await CMMCommand.PowerOn(sn); + else + await CMMCommand.Sleep(sn); + await Task.Delay(1000); + btn.Content = await CMMCommand.GetMonPowerStatus(sn); + } + }; + powerPanel.Children.Add(powerBtn); + container.Children.Add(powerPanel); + + return container; + } + + private StackPanel CreateControl( + XMonitor monitorModel, + int? brightness, + int? contrast, + int? currentInputSource, + List inputOptions, + string powerStatus) + { + var container = new StackPanel + { + Orientation = Orientation.Vertical, + Margin = new Thickness(10, 5, 10, 10) + }; + + // Monitor name header + var header = new TextBlock + { + Text = $"{monitorModel.MonitorName} ({monitorModel.SerialNumber})", HorizontalAlignment = HorizontalAlignment.Left, + Style = (Style)FindResource("LableStyle"), + Margin = new Thickness(0, 0, 0, 5) + }; + container.Children.Add(header); + + // Brightness slider + var brightnessPanel = CreateSliderControl( + Lang.Find("Brightness"), + brightness ?? 50, + monitorModel.SerialNumber, + async (sn, value) => await CMMCommand.SetBrightness(sn, value)); + container.Children.Add(brightnessPanel); + + // Contrast slider + var contrastPanel = CreateSliderControl( + Lang.Find("Contrast"), + contrast ?? 50, + monitorModel.SerialNumber, + async (sn, value) => await CMMCommand.SetContrast(sn, value)); + container.Children.Add(contrastPanel); + + // Input source dropdown + if (inputOptions.Count > 0) + { + var inputPanel = CreateInputSourceControl( + monitorModel.SerialNumber, + currentInputSource, + inputOptions); + container.Children.Add(inputPanel); + } + + // Power button + var powerPanel = CreatePowerControl(monitorModel.SerialNumber, powerStatus); + container.Children.Add(powerPanel); + + return container; + } + + private StackPanel CreateSliderControl( + string label, + int currentValue, + string monitorSN, + Func onValueChanged) + { + var panel = new StackPanel + { + Orientation = Orientation.Horizontal, + Margin = new Thickness(0, 3, 0, 3) + }; + + var labelBlock = new TextBlock + { + Text = label, + Width = 80, + VerticalAlignment = VerticalAlignment.Center, + Style = (Style)FindResource("LableStyle") + }; + + var slider = new Slider + { + Style = (Style)FindResource("Horizontal_Slider"), + Minimum = 0, + Maximum = 100, + Value = currentValue, + Tag = monitorSN, + Width = 180 + }; + + var valueBlock = new TextBlock + { + Text = currentValue.ToString(), + Width = 35, + VerticalAlignment = VerticalAlignment.Center, + Style = (Style)FindResource("LableStyle"), + TextAlignment = TextAlignment.Right + }; + + // Update value display while dragging + slider.ValueChanged += (s, e) => + { + valueBlock.Text = ((int)e.NewValue).ToString(); + }; + + // Only send command on release (PreviewMouseUp) + slider.PreviewMouseUp += async (s, e) => + { + var sld = s as Slider; + var sn = sld?.Tag?.ToString(); + if (!string.IsNullOrEmpty(sn) && sld != null) + { + await onValueChanged(sn, (int)sld.Value); + } + }; + + panel.Children.Add(labelBlock); + panel.Children.Add(slider); + panel.Children.Add(valueBlock); + + return panel; + } + + private StackPanel CreateInputSourceControl( + string monitorSN, + int? currentInputSource, + List options) + { + var panel = new StackPanel + { + Orientation = Orientation.Horizontal, + Margin = new Thickness(0, 3, 0, 3) + }; + + var labelBlock = new TextBlock + { + Text = Lang.Find("InputSource"), + Width = 80, + VerticalAlignment = VerticalAlignment.Center, + Style = (Style)FindResource("LableStyle") + }; + + var comboBox = new ComboBox + { + Width = 180, + Tag = monitorSN, + ItemsSource = options, + DisplayMemberPath = "Name" + }; + + // Set current selection + if (currentInputSource.HasValue) + { + var currentOption = options.Find(o => o.Value == currentInputSource.Value); + if (currentOption != null) + { + comboBox.SelectedItem = currentOption; + } + } + + comboBox.SelectionChanged += async (s, e) => + { + var cb = s as ComboBox; + var sn = cb?.Tag?.ToString(); + var selectedOption = cb?.SelectedItem as InputSourceOption; + + if (!string.IsNullOrEmpty(sn) && selectedOption != null) + { + await CMMCommand.SetInputSource(sn, selectedOption.Value); + } + }; + + panel.Children.Add(labelBlock); + panel.Children.Add(comboBox); + + return panel; + } + + private StackPanel CreatePowerControl(string monitorSN, string powerStatus) + { + var panel = new StackPanel + { + Orientation = Orientation.Horizontal, + Margin = new Thickness(0, 5, 0, 0) + }; + + var labelBlock = new TextBlock + { + Text = Lang.Find("Power"), + Width = 80, + VerticalAlignment = VerticalAlignment.Center, Style = (Style)FindResource("LableStyle") }; var btn = new Button { - Tag = monitorModel.SerialNumber, - Content = powerStatus, - Style = (Style)FindResource("TextButtonStyle") + Tag = monitorSN, + Content = GetLocalizedPowerStatus(powerStatus), + Style = (Style)FindResource("TextButtonStyle"), + Width = 180 }; - btn.Click += async (s, e) => await ToggleButton_Checked(s, e); + btn.Click += async (s, e) => await TogglePower(s, e); - _sp.Children.Add(tb); - _sp.Children.Add(btn); + panel.Children.Add(labelBlock); + panel.Children.Add(btn); - return _sp; + return panel; } - private void Border_MouseLeave(object sender, MouseEventArgs e) + private string GetLocalizedPowerStatus(string status) { - + return status switch + { + "PowerOn" => Lang.Find("PowerOn"), + "Sleep" => Lang.Find("Sleep"), + "PowerOff" => Lang.Find("PowerOff"), + _ => status + }; } - private async Task ToggleButton_Checked(object sender, RoutedEventArgs e) + private async Task TogglePower(object sender, RoutedEventArgs e) { var btn = sender as Button; - var tag = btn?.Tag.ToString(); - var content = btn?.Content as string; - if (content == "Sleep") - { + var tag = btn?.Tag?.ToString(); + if (string.IsNullOrEmpty(tag)) return; + + var currentStatus = await CMMCommand.GetMonPowerStatus(tag); + + if (currentStatus == "Sleep" || currentStatus == "PowerOff") + { await CMMCommand.PowerOn(tag); } else @@ -80,6 +459,34 @@ public partial class ControlPanel : UserControl } await Task.Delay(1000); - btn!.Content = await CMMCommand.GetMonPowerStatus(tag); + var newStatus = await CMMCommand.GetMonPowerStatus(tag); + btn!.Content = GetLocalizedPowerStatus(newStatus); + } + + private void Border_MouseLeave(object sender, MouseEventArgs e) + { + } + + private void UserControl_Loaded(object sender, RoutedEventArgs e) + { + // Use Dispatcher to let the popup fully render first + Dispatcher.BeginInvoke(new Action(async () => + { + try + { + await Refresh(); + } + catch (Exception ex) + { + sp.Children.Clear(); + sp.Children.Add(new TextBlock + { + Text = $"Error: {ex.Message}", + Foreground = System.Windows.Media.Brushes.Red, + FontSize = 12, + TextWrapping = TextWrapping.Wrap + }); + } + }), System.Windows.Threading.DispatcherPriority.Background); } } diff --git a/DellMonitorControl/DellMonitorControl.csproj b/DellMonitorControl/DellMonitorControl.csproj index ef6ecf3..8e96557 100644 --- a/DellMonitorControl/DellMonitorControl.csproj +++ b/DellMonitorControl/DellMonitorControl.csproj @@ -2,7 +2,7 @@ WinExe - net7.0-windows + net9.0-windows enable DellMonitorControl DellMonitorControl @@ -45,6 +45,7 @@ + diff --git a/DellMonitorControl/MainWindow.xaml b/DellMonitorControl/MainWindow.xaml index 3b4f91c..ceda82f 100644 --- a/DellMonitorControl/MainWindow.xaml +++ b/DellMonitorControl/MainWindow.xaml @@ -1,21 +1,15 @@  - - - - - - - - + Title="Monitor Control" Height="400" Width="320" + WindowStyle="ToolWindow" + ResizeMode="CanResize" + Background="#333333"> + + + + + + + diff --git a/DellMonitorControl/MainWindow.xaml.cs b/DellMonitorControl/MainWindow.xaml.cs index 2399cde..44ff5b4 100644 --- a/DellMonitorControl/MainWindow.xaml.cs +++ b/DellMonitorControl/MainWindow.xaml.cs @@ -1,22 +1,171 @@ -using System.Threading.Tasks; +using CMM.Library.Method; +using CMM.Library.ViewModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; namespace DellMonitorControl; -/// -/// Interaction logic for MainWindow.xaml -/// public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); - this.Hide(); - taskbar.TrayPopupOpen += async (s, e) => await Taskbar_TrayPopupOpen(s, e); + PositionWindowNearTray(); + Loaded += async (s, e) => await LoadMonitors(); } - private async Task Taskbar_TrayPopupOpen(object sender, RoutedEventArgs e) + private void PositionWindowNearTray() { - await comtrolPanel.Refresh(); + var workArea = SystemParameters.WorkArea; + Left = workArea.Right - Width - 10; + Top = workArea.Bottom - Height - 10; + } + + private static readonly string LogFile = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "MonitorControl.log"); + + private static void Log(string msg) + { + try { System.IO.File.AppendAllText(LogFile, $"{DateTime.Now:HH:mm:ss.fff} {msg}\n"); } + catch { } + } + + private async Task LoadMonitors() + { + var newChildren = new List(); + Log("LoadMonitors started"); + + try + { + Log("Calling ScanMonitor..."); + await CMMCommand.ScanMonitor(); + Log("ScanMonitor complete"); + + Log("Calling ReadMonitorsData..."); + var monitors = (await CMMCommand.ReadMonitorsData()).ToList(); + Log($"ReadMonitorsData complete, found {monitors.Count} monitors"); + + if (!monitors.Any()) + { + Log("No monitors found"); + newChildren.Add(new TextBlock { Text = "No DDC/CI monitors detected", Foreground = Brushes.White, FontSize = 14 }); + } + else + { + foreach (var m in monitors) + { + Log($"Processing monitor: {m.MonitorName} ({m.SerialNumber})"); + + var brightness = await CMMCommand.GetBrightness(m.SerialNumber) ?? 50; + Log($" Brightness: {brightness}"); + var contrast = await CMMCommand.GetContrast(m.SerialNumber) ?? 50; + Log($" Contrast: {contrast}"); + var inputSource = await CMMCommand.GetInputSource(m.SerialNumber); + Log($" InputSource: {inputSource}"); + var inputOptions = await CMMCommand.GetInputSourceOptions(m.SerialNumber); + Log($" InputOptions count: {inputOptions.Count}"); + var powerStatus = await CMMCommand.GetMonPowerStatus(m.SerialNumber) ?? "Unknown"; + Log($" PowerStatus: {powerStatus}"); + + newChildren.Add(new TextBlock + { + Text = m.MonitorName, + Foreground = Brushes.White, + FontSize = 14, + FontWeight = FontWeights.Bold, + Margin = new Thickness(0, 10, 0, 5) + }); + + newChildren.Add(CreateSliderRow("Brightness", brightness, m.SerialNumber, CMMCommand.SetBrightness)); + newChildren.Add(CreateSliderRow("Contrast", contrast, m.SerialNumber, CMMCommand.SetContrast)); + + if (inputOptions.Count > 0) + newChildren.Add(CreateInputRow(inputSource, inputOptions, m.SerialNumber)); + + newChildren.Add(CreatePowerRow(powerStatus, m.SerialNumber)); + } + } + Log($"Built {newChildren.Count} UI elements"); + } + catch (Exception ex) + { + Log($"ERROR: {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}"); + newChildren.Clear(); + newChildren.Add(new TextBlock { Text = $"Error: {ex.Message}", Foreground = Brushes.Red, FontSize = 12, TextWrapping = TextWrapping.Wrap }); + } + + Log("Updating UI..."); + sp.Children.Clear(); + foreach (var child in newChildren) + sp.Children.Add(child); + Log($"UI updated with {sp.Children.Count} children"); + } + + private StackPanel CreateSliderRow(string label, int value, string serialNumber, Func setCommand) + { + var row = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) }; + row.Children.Add(new TextBlock { Text = label, Foreground = Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center }); + + var slider = new Slider { Minimum = 0, Maximum = 100, Value = value, Width = 140, Tag = serialNumber }; + var valueText = new TextBlock { Text = value.ToString(), Foreground = Brushes.White, Width = 30, FontSize = 12, Margin = new Thickness(5, 0, 0, 0) }; + + slider.ValueChanged += (s, e) => valueText.Text = ((int)e.NewValue).ToString(); + slider.PreviewMouseUp += async (s, e) => + { + if (s is Slider sl && sl.Tag is string sn) + await setCommand(sn, (int)sl.Value); + }; + + row.Children.Add(slider); + row.Children.Add(valueText); + return row; + } + + private StackPanel CreateInputRow(int? currentInput, List options, string serialNumber) + { + var row = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) }; + row.Children.Add(new TextBlock { Text = "Input", Foreground = Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center }); + + var combo = new ComboBox { Width = 170, ItemsSource = options, DisplayMemberPath = "Name", Tag = serialNumber }; + if (currentInput.HasValue) + combo.SelectedItem = options.Find(o => o.Value == currentInput.Value); + + combo.SelectionChanged += async (s, e) => + { + if (s is ComboBox cb && cb.Tag is string sn && cb.SelectedItem is InputSourceOption opt) + await CMMCommand.SetInputSource(sn, opt.Value); + }; + + row.Children.Add(combo); + return row; + } + + private StackPanel CreatePowerRow(string status, string serialNumber) + { + var row = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 3) }; + row.Children.Add(new TextBlock { Text = "Power", Foreground = Brushes.LightGray, Width = 70, FontSize = 12, VerticalAlignment = VerticalAlignment.Center }); + + var btn = new Button { Content = status, Width = 170, Tag = serialNumber }; + btn.Click += async (s, e) => + { + if (s is Button b && b.Tag is string sn) + { + var current = await CMMCommand.GetMonPowerStatus(sn); + if (current == "Sleep" || current == "PowerOff") + await CMMCommand.PowerOn(sn); + else + await CMMCommand.Sleep(sn); + await Task.Delay(1000); + b.Content = await CMMCommand.GetMonPowerStatus(sn); + } + }; + + row.Children.Add(btn); + return row; } } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4f35837 --- /dev/null +++ b/LICENSE @@ -0,0 +1,37 @@ +MIT License (for new contributions only) + +Copyright (c) 2026 David H Friedel Jr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +NOTICE: This project contains components with different licensing terms: + +1. Original code by DangWang - No explicit license was provided with the + original repository. Use at your own discretion. + +2. ControlMyMonitor.exe by Nir Sofer (NirSoft) - Freeware license: + - Free distribution allowed + - No commercial use or sale + - No modification + - See: https://www.nirsoft.net + +3. New contributions (brightness/contrast controls, input source switching, + 9-language localization) - MIT License as above. diff --git a/Language/CulturesHelper.cs b/Language/CulturesHelper.cs index 9206b00..cb7e678 100644 --- a/Language/CulturesHelper.cs +++ b/Language/CulturesHelper.cs @@ -54,12 +54,48 @@ namespace CMM.Language var Languages = new List(); string uriPath = ResourceLocalPath + "/" + LanguageFileName; - Languages.Add(uriPath + ".en-US.xaml"); - Languages.Add(uriPath + ".zh-TW.xaml"); + Languages.Add(uriPath + ".en.xaml"); + Languages.Add(uriPath + ".es.xaml"); + Languages.Add(uriPath + ".fr.xaml"); + Languages.Add(uriPath + ".de.xaml"); + Languages.Add(uriPath + ".zh.xaml"); + Languages.Add(uriPath + ".ja.xaml"); + Languages.Add(uriPath + ".pt.xaml"); + Languages.Add(uriPath + ".it.xaml"); + Languages.Add(uriPath + ".hi.xaml"); return Languages; } + /// + /// Auto-detect and apply system language + /// + public void ApplySystemLanguage() + { + var systemCulture = CultureInfo.CurrentUICulture; + var matchedCulture = FindBestMatchingCulture(systemCulture); + ChangeCulture(matchedCulture); + } + + /// + /// Find the best matching supported culture for the given culture + /// + private CultureInfo FindBestMatchingCulture(CultureInfo culture) + { + // Try exact match first + var exactMatch = SupportedCultures.FirstOrDefault(c => + c.Name.Equals(culture.Name, StringComparison.OrdinalIgnoreCase)); + if (exactMatch != null) return exactMatch; + + // Try matching by two-letter ISO language name + var languageMatch = SupportedCultures.FirstOrDefault(c => + c.TwoLetterISOLanguageName.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase)); + if (languageMatch != null) return languageMatch; + + // Default to English + return SupportedCultures.FirstOrDefault(c => c.Name == "en") ?? SupportedCultures.First(); + } + public void ChangeCulture(string cultureName) { var cultureInfo = CultureInfo.GetCultureInfo(cultureName); diff --git a/Language/Language.csproj b/Language/Language.csproj index 9b57b5f..1d47fcb 100644 --- a/Language/Language.csproj +++ b/Language/Language.csproj @@ -1,7 +1,7 @@  - net7.0-windows + net9.0-windows CMM.Language enable true @@ -22,13 +22,34 @@ - - MSBuild:Compile - MSBuild:Compile - + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + MSBuild:Compile diff --git a/Language/StringResources.de.xaml b/Language/StringResources.de.xaml new file mode 100644 index 0000000..c5ac9c2 --- /dev/null +++ b/Language/StringResources.de.xaml @@ -0,0 +1,29 @@ + + + + Monitorsteuerung + Bildschirmschalter + + + Helligkeit + Kontrast + Eingangsquelle + Stromversorgung + + + Einschalten + Ruhezustand + Ausschalten + + + Minimieren + Beenden + Laden... + + + Fehler beim Laden der Konfiguration: + Fehler beim Speichern der Konfiguration: + + diff --git a/Language/StringResources.en.xaml b/Language/StringResources.en.xaml new file mode 100644 index 0000000..45aa87b --- /dev/null +++ b/Language/StringResources.en.xaml @@ -0,0 +1,29 @@ + + + + Monitor Control Management + Screen Switch + + + Brightness + Contrast + Input Source + Power + + + Power On + Sleep + Power Off + + + Minimize + Exit + Loading... + + + Failed to load configuration: + Failed to save configuration: + + diff --git a/Language/StringResources.es.xaml b/Language/StringResources.es.xaml new file mode 100644 index 0000000..8705404 --- /dev/null +++ b/Language/StringResources.es.xaml @@ -0,0 +1,29 @@ + + + + Control de Monitor + Interruptor de Pantalla + + + Brillo + Contraste + Fuente de Entrada + Encendido + + + Encender + Suspender + Apagar + + + Minimizar + Salir + Cargando... + + + Error al cargar la configuración: + Error al guardar la configuración: + + diff --git a/Language/StringResources.fr.xaml b/Language/StringResources.fr.xaml new file mode 100644 index 0000000..a5e896e --- /dev/null +++ b/Language/StringResources.fr.xaml @@ -0,0 +1,29 @@ + + + + Gestion du Moniteur + Interrupteur d'Écran + + + Luminosité + Contraste + Source d'Entrée + Alimentation + + + Allumer + Veille + Éteindre + + + Réduire + Quitter + Chargement... + + + Échec du chargement de la configuration: + Échec de la sauvegarde de la configuration: + + diff --git a/Language/StringResources.hi.xaml b/Language/StringResources.hi.xaml new file mode 100644 index 0000000..61b480e --- /dev/null +++ b/Language/StringResources.hi.xaml @@ -0,0 +1,29 @@ + + + + मॉनिटर नियंत्रण प्रबंधन + स्क्रीन स्विच + + + चमक + कंट्रास्ट + इनपुट स्रोत + पावर + + + चालू करें + स्लीप + बंद करें + + + छोटा करें + बाहर निकलें + लोड हो रहा है... + + + कॉन्फ़िगरेशन लोड करने में विफल: + कॉन्फ़िगरेशन सहेजने में विफल: + + diff --git a/Language/StringResources.it.xaml b/Language/StringResources.it.xaml new file mode 100644 index 0000000..bf298a7 --- /dev/null +++ b/Language/StringResources.it.xaml @@ -0,0 +1,29 @@ + + + + Gestione Monitor + Interruttore Schermo + + + Luminosità + Contrasto + Sorgente di Ingresso + Alimentazione + + + Accendi + Sospendi + Spegni + + + Riduci a icona + Esci + Caricamento... + + + Impossibile caricare la configurazione: + Impossibile salvare la configurazione: + + diff --git a/Language/StringResources.ja.xaml b/Language/StringResources.ja.xaml new file mode 100644 index 0000000..0ab4492 --- /dev/null +++ b/Language/StringResources.ja.xaml @@ -0,0 +1,29 @@ + + + + モニター制御管理 + 画面スイッチ + + + 輝度 + コントラスト + 入力ソース + 電源 + + + 電源オン + スリープ + 電源オフ + + + 最小化 + 終了 + 読み込み中... + + + 設定の読み込みに失敗しました: + 設定の保存に失敗しました: + + diff --git a/Language/StringResources.pt.xaml b/Language/StringResources.pt.xaml new file mode 100644 index 0000000..6ff8475 --- /dev/null +++ b/Language/StringResources.pt.xaml @@ -0,0 +1,29 @@ + + + + Controle de Monitor + Interruptor de Tela + + + Brilho + Contraste + Fonte de Entrada + Energia + + + Ligar + Suspender + Desligar + + + Minimizar + Sair + Carregando... + + + Falha ao carregar a configuração: + Falha ao salvar a configuração: + + diff --git a/Language/StringResources.xaml b/Language/StringResources.xaml index 603c9e6..02051ac 100644 --- a/Language/StringResources.xaml +++ b/Language/StringResources.xaml @@ -1,5 +1,29 @@ - - - \ No newline at end of file + + + Monitor Control Management + Screen Switch + + + Brightness + Contrast + Input Source + Power + + + Power On + Sleep + Power Off + + + Minimize + Exit + Loading... + + + Failed to load configuration: + Failed to save configuration: + + diff --git a/Language/StringResources.zh.xaml b/Language/StringResources.zh.xaml new file mode 100644 index 0000000..8fd1e4e --- /dev/null +++ b/Language/StringResources.zh.xaml @@ -0,0 +1,29 @@ + + + + 螢幕控制管理 + 螢幕開關 + + + 亮度 + 對比度 + 輸入源 + 電源 + + + 開啟 + 休眠 + 關閉 + + + 最小化 + 結束 + 載入中... + + + 載入設定檔失敗: + 保存設定檔失敗: + + diff --git a/Library/Library.csproj b/Library/Library.csproj index 8955688..bb8b4ad 100644 --- a/Library/Library.csproj +++ b/Library/Library.csproj @@ -1,7 +1,7 @@ - net7.0-windows + net9.0-windows enable CMM.Library CMM.Library diff --git a/Library/Method/CMMCommand.cs b/Library/Method/CMMCommand.cs index a6a2cb4..2c94ac3 100644 --- a/Library/Method/CMMCommand.cs +++ b/Library/Method/CMMCommand.cs @@ -30,13 +30,13 @@ public static class CMMCommand return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} D6 4"); } - private static async Task GetMonitorValue(string monitorSN, int? reTry = 0) + private static async Task 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 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 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 GetInputSource(string monitorSN) + { + var value = await GetMonitorValue(monitorSN, "60"); + return int.TryParse(value, out var result) ? result : null; + } + + public static async Task> GetInputSourceOptions(string monitorSN) + { + var options = new List(); + 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>(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 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"); + /// /// 取得螢幕清單 /// @@ -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; } diff --git a/Library/ViewModel/XMonitorStatus.cs b/Library/ViewModel/XMonitorStatus.cs index 3c6e27a..f7e7138 100644 --- a/Library/ViewModel/XMonitorStatus.cs +++ b/Library/ViewModel/XMonitorStatus.cs @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..f9786a3 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# ControlMyMonitorManagement + +A Windows desktop application for controlling monitor settings via DDC/CI (Display Data Channel/Command Interface). Adjust brightness, contrast, power state, and other display parameters across multiple monitors. + +## Features + +- **Multi-Monitor Support** - Detect and control multiple connected displays +- **Brightness & Contrast Sliders** - Easy adjustment with real-time feedback +- **Input Source Switching** - Switch between VGA, DVI, HDMI, DisplayPort (auto-detected from monitor) +- **Power Management** - Turn monitors on, off, or into sleep mode +- **System Tray Integration** - DellMonitorControl app with taskbar tray popup +- **9 Languages** - Auto-detects system language (en, es, fr, de, zh, ja, pt, it, hi) + +## Requirements + +- Windows OS +- .NET 7.0 SDK +- DDC/CI compatible monitor(s) + +## Build + +```bash +# Clone the repository +git clone https://github.com/yourusername/ControlMyMonitorManagement.git +cd ControlMyMonitorManagement + +# Build the solution +dotnet build ControlMyMonitorManagement.sln + +# Run the main application +dotnet run --project ControlMyMonitorManagement/ControlMyMonitorManagement.csproj + +# Run the Dell-specific application (with system tray) +dotnet run --project DellMonitorControl/DellMonitorControl.csproj +``` + +## Project Structure + +| Project | Description | +|---------|-------------| +| **ControlMyMonitorManagement** | Main WPF application with full UI | +| **DellMonitorControl** | Lightweight app with system tray integration | +| **Library** | Core business logic and DDC/CI operations | +| **CMMModel** | Data models for monitor status | +| **CMMService** | Service layer interfaces | +| **Language** | Localization support | +| **Tester** | NUnit test project | + +## Common VCP Codes + +| Code | Name | Values | +|------|------|--------| +| D6 | Power Mode | 1=On, 4=Sleep, 5=Off | +| 10 | Brightness | 0-100 | +| 12 | Contrast | 0-100 | +| 60 | Input Source | 1=VGA, 3=DVI, 15=DisplayPort, 17=HDMI | + +## How It Works + +This application uses DDC/CI protocol to communicate with monitors through the display cable (HDMI, DisplayPort, DVI, VGA). DDC/CI allows software to send commands directly to the monitor's firmware to adjust settings that would normally require using the monitor's physical buttons. + +## Credits + +- **Original Project**: [DangWang](https://github.com/poyingHAHA) - ControlMyMonitorManagement +- **DDC/CI Tool**: [Nir Sofer](https://www.nirsoft.net) - ControlMyMonitor.exe (freeware) +- **Enhancements by David H Friedel Jr**: Brightness/contrast sliders, input source switching, 9-language localization + +## License + +This project contains multiple components with different terms: + +- **Original code by DangWang**: No explicit license provided +- **ControlMyMonitor.exe**: Freeware by Nir Sofer - free distribution allowed, no commercial use, no modification +- **New contributions (sliders, localization, etc.)**: MIT License diff --git a/Tester/Tester.csproj b/Tester/Tester.csproj index ce9ba59..fb347fe 100644 --- a/Tester/Tester.csproj +++ b/Tester/Tester.csproj @@ -1,7 +1,7 @@ - net7.0-windows + net9.0-windows enable CMM.Tester ControlMyMonitorManagement